แลมบ์ดาต้นไม้นิพจน์ต้องไม่มีตัวดำเนินการขยายสัญญาณว่าง


96

คำถาม : บรรทัดprice = co?.price ?? 0,ในรหัสต่อไปนี้ทำให้ฉันมีข้อผิดพลาดข้างต้น แต่ถ้าผมลบ?จากการco.?ทำงานดี ผมพยายามที่จะทำตามตัวอย่างนี้ MSDNที่พวกเขาจะใช้?ในบรรทัดselect new { person.FirstName, PetName = subpet?.Name ?? String.Empty };ดังนั้นจึงดูเหมือนว่าฉันจะต้องเข้าใจเมื่อใช้?กับ??และเมื่อไม่

ข้อผิดพลาด :

แลมบ์ดาต้นไม้นิพจน์ต้องไม่มีตัวดำเนินการขยายสัญญาณว่าง

public class CustomerOrdersModelView
{
    public string CustomerID { get; set; }
    public int FY { get; set; }
    public float? price { get; set; }
    ....
    ....
}
public async Task<IActionResult> ProductAnnualReport(string rpt)
{
    var qry = from c in _context.Customers
              join ord in _context.Orders
                on c.CustomerID equals ord.CustomerID into co
              from m in co.DefaultIfEmpty()
              select new CustomerOrdersModelView
              {
                  CustomerID = c.CustomerID,
                  FY = c.FY,
                  price = co?.price ?? 0,
                  ....
                  ....
              };
    ....
    ....
 }

กรุณาโพสต์ข้อผิดพลาด ...
Willem Van Onsem

3
ฉันอยากให้ C # สนับสนุนสิ่งนี้!
nawfal

คำตอบ:


150

ตัวอย่างที่คุณได้รับข้อความจากการใช้ LINQ กับวัตถุที่แสดงออกแลมบ์ดานัยในแบบสอบถามจะถูกแปลงเป็นผู้ได้รับมอบหมาย ... ในขณะที่คุณกำลังใช้ EF หรือคล้ายกันกับIQueryable<T>queryies ที่แสดงออกแลมบ์ดาจะถูกแปลงเป็นต้นไม้แสดงออก แผนภูมินิพจน์ไม่รองรับตัวดำเนินการตามเงื่อนไขว่าง (หรือทูเปิล)

เพียงแค่ทำแบบเก่า:

price = co == null ? 0 : (co.price ?? 0)

(ฉันเชื่อว่าตัวดำเนินการการรวมกันเป็นโมฆะนั้นใช้ได้ดีในแผนภูมินิพจน์)


ในกรณีที่คุณใช้ Dynamic LINQ (System.Linq.Dynamic.Core) คุณสามารถใช้np()เมธอด ดูgithub.com/StefH/System.Linq.Dynamic.Core/wiki/NullPropagation
Stef

11

List<T>รหัสคุณเชื่อมโยงกับการใช้งาน List<T>การดำเนินการแต่ไม่IEnumerable<T> IQueryable<T>ในกรณีนั้นการฉายภาพจะดำเนินการในหน่วยความจำและใช้?.งานได้

คุณกำลังใช้งานบางอย่างIQueryable<T>ซึ่งทำงานแตกต่างกันมาก สำหรับIQueryable<T>การเป็นตัวแทนของการฉายภาพจะถูกสร้างขึ้นและผู้ให้บริการ LINQ ของคุณจะตัดสินใจว่าจะทำอย่างไรกับรันไทม์ ด้วยเหตุผลด้านความเข้ากันได้ย้อนหลัง?.ไม่สามารถใช้ที่นี่ได้

ทั้งนี้ขึ้นอยู่กับผู้ให้บริการ LINQ ของคุณคุณอาจจะสามารถใช้ธรรมดาและยังคงไม่ได้รับการใด ๆ.NullReferenceException


@hvd คุณช่วยอธิบายได้ไหมว่าทำไมสิ่งนี้จึงจำเป็นสำหรับความเข้ากันได้แบบย้อนกลับ?
jag

1
@jag ผู้ให้บริการ LINQ ทั้งหมดที่ถูกสร้างขึ้นก่อนการเปิดตัว?.จะไม่ได้รับการเตรียมการเพื่อจัดการ?.ในลักษณะที่สมเหตุสมผล

1
แต่?.เป็นตัวดำเนินการใหม่ไม่ใช่หรือ ดังนั้นรหัสเก่าจะไม่ใช้?.และไม่เสีย ผู้ให้บริการ Linq ไม่ได้เตรียมพร้อมที่จะจัดการกับสิ่งอื่น ๆ เช่นวิธี CLR
jag

2
@jag ถูกต้องรหัสเก่าร่วมกับผู้ให้บริการ LINQ เก่าจะไม่ได้รับผลกระทบ ?.รหัสเดิมจะไม่ได้ใช้ รหัสใหม่อาจใช้ผู้ให้บริการ LINQ เก่าซึ่งมีการเตรียมที่จะจัดการกับวิธีการ CLR พวกเขาไม่รู้จัก (โดยการขว้างปายกเว้น) ตั้งแต่พอดีเหล่านั้นอย่างเป็นรูปแบบวัตถุต้นไม้แสดงออกที่มีอยู่ ประเภทโหนดแผนภูมินิพจน์ใหม่ทั้งหมดไม่พอดี

3
เมื่อพิจารณาถึงจำนวนข้อยกเว้นที่ผู้ให้บริการ LINQ โยนไว้แล้วดูเหมือนแทบจะไม่คุ้มค่าเลย - "เราเคยไม่สนับสนุนดังนั้นเราจึงไม่สามารถทำได้"
NetMage

1

คำตอบของ Jon Skeet ถูกต้องในกรณีของฉันฉันใช้DateTimeสำหรับคลาสเอนทิตีของฉัน เมื่อได้ลองใช้เช่น

(a.DateProperty == null ? default : a.DateProperty.Date)

ฉันมีข้อผิดพลาด

Property 'System.DateTime Date' is not defined for type 'System.Nullable`1[System.DateTime]' (Parameter 'property')

ดังนั้นฉันจำเป็นต้องเปลี่ยนDateTime?สำหรับคลาสเอนทิตีของฉันและ

(a.DateProperty == null ? default : a.DateProperty.Value.Date)

สิ่งนี้ไม่เกี่ยวกับตัวดำเนินการการขยายสัญญาณว่าง
Gert Arnold

ฉันชอบที่คุณพูดว่า Jon Skeet พูดถูกโดยบอกว่าเป็นไปได้ที่เขาจะคิดผิด สิ่งที่ดี!
Klicker

0

ในขณะที่แผนภูมินิพจน์ไม่สนับสนุนการแพร่กระจายแบบ null C # 6.0 สิ่งที่เราทำได้คือสร้างผู้เยี่ยมชมที่ปรับเปลี่ยนโครงสร้างนิพจน์สำหรับการขยายสัญญาณว่างเปล่าเช่นเดียวกับที่ตัวดำเนินการทำ!

นี่คือของฉัน:

public class NullPropagationVisitor : ExpressionVisitor
{
    private readonly bool _recursive;

    public NullPropagationVisitor(bool recursive)
    {
        _recursive = recursive;
    }

    protected override Expression VisitUnary(UnaryExpression propertyAccess)
    {
        if (propertyAccess.Operand is MemberExpression mem)
            return VisitMember(mem);

        if (propertyAccess.Operand is MethodCallExpression met)
            return VisitMethodCall(met);

        if (propertyAccess.Operand is ConditionalExpression cond)
            return Expression.Condition(
                    test: cond.Test,
                    ifTrue: MakeNullable(Visit(cond.IfTrue)),
                    ifFalse: MakeNullable(Visit(cond.IfFalse)));

        return base.VisitUnary(propertyAccess);
    }

    protected override Expression VisitMember(MemberExpression propertyAccess)
    {
        return Common(propertyAccess.Expression, propertyAccess);
    }

    protected override Expression VisitMethodCall(MethodCallExpression propertyAccess)
    {
        if (propertyAccess.Object == null)
            return base.VisitMethodCall(propertyAccess);

        return Common(propertyAccess.Object, propertyAccess);
    }

    private BlockExpression Common(Expression instance, Expression propertyAccess)
    {
        var safe = _recursive ? base.Visit(instance) : instance;
        var caller = Expression.Variable(safe.Type, "caller");
        var assign = Expression.Assign(caller, safe);
        var acess = MakeNullable(new ExpressionReplacer(instance,
            IsNullableStruct(instance) ? caller : RemoveNullable(caller)).Visit(propertyAccess));
        var ternary = Expression.Condition(
                    test: Expression.Equal(caller, Expression.Constant(null)),
                    ifTrue: Expression.Constant(null, acess.Type),
                    ifFalse: acess);

        return Expression.Block(
            type: acess.Type,
            variables: new[]
            {
                caller,
            },
            expressions: new Expression[]
            {
                assign,
                ternary,
            });
    }

    private static Expression MakeNullable(Expression ex)
    {
        if (IsNullable(ex))
            return ex;

        return Expression.Convert(ex, typeof(Nullable<>).MakeGenericType(ex.Type));
    }

    private static bool IsNullable(Expression ex)
    {
        return !ex.Type.IsValueType || (Nullable.GetUnderlyingType(ex.Type) != null);
    }

    private static bool IsNullableStruct(Expression ex)
    {
        return ex.Type.IsValueType && (Nullable.GetUnderlyingType(ex.Type) != null);
    }

    private static Expression RemoveNullable(Expression ex)
    {
        if (IsNullableStruct(ex))
            return Expression.Convert(ex, ex.Type.GenericTypeArguments[0]);

        return ex;
    }

    private class ExpressionReplacer : ExpressionVisitor
    {
        private readonly Expression _oldEx;
        private readonly Expression _newEx;

        internal ExpressionReplacer(Expression oldEx, Expression newEx)
        {
            _oldEx = oldEx;
            _newEx = newEx;
        }

        public override Expression Visit(Expression node)
        {
            if (node == _oldEx)
                return _newEx;

            return base.Visit(node);
        }
    }
}

ผ่านการทดสอบต่อไปนี้:

private static string Foo(string s) => s;

static void Main(string[] _)
{
    var visitor = new NullPropagationVisitor(recursive: true);

    Test1();
    Test2();
    Test3();

    void Test1()
    {
        Expression<Func<string, char?>> f = s => s == "foo" ? 'X' : Foo(s).Length.ToString()[0];

        var fBody = (Expression<Func<string, char?>>)visitor.Visit(f);

        var fFunc = fBody.Compile();

        Debug.Assert(fFunc(null) == null);
        Debug.Assert(fFunc("bar") == '3');
        Debug.Assert(fFunc("foo") == 'X');
    }

    void Test2()
    {
        Expression<Func<string, int>> y = s => s.Length;

        var yBody = visitor.Visit(y.Body);
        var yFunc = Expression.Lambda<Func<string, int?>>(
                                    body: yBody,
                                    parameters: y.Parameters)
                            .Compile();

        Debug.Assert(yFunc(null) == null);
        Debug.Assert(yFunc("bar") == 3);
    }

    void Test3()
    {
        Expression<Func<char?, string>> y = s => s.Value.ToString()[0].ToString();

        var yBody = visitor.Visit(y.Body);
        var yFunc = Expression.Lambda<Func<char?, string>>(
                                    body: yBody,
                                    parameters: y.Parameters)
                            .Compile();

        Debug.Assert(yFunc(null) == null);
        Debug.Assert(yFunc('A') == "A");
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.