首页 文章

Expression.Quote()做了什么,Expression.Constant()不能做什么?

提问于
浏览
92

Note: I am aware of the earlier question “What is the purpose of LINQ's Expression.Quote method?” ,但如果你继续阅读,你会看到它没有回答我的问题 .

我明白 Expression.Quote() 的陈述目的是什么 . 但是, Expression.Constant() 可以用于相同的目的(除了已经使用 Expression.Constant() 的所有目的) . 因此,我不明白为什么需要 Expression.Quote() .

为了证明这一点,我写了一个快速的例子,其中一个人习惯使用 Quote (参见标有感叹号的行),但我使用了 Constant 而且它同样有效:

string[] array = { "one", "two", "three" };

// This example constructs an expression tree equivalent to the lambda:
// str => str.AsQueryable().Any(ch => ch == 'e')

Expression<Func<char, bool>> innerLambda = ch => ch == 'e';

var str = Expression.Parameter(typeof(string), "str");
var expr =
    Expression.Lambda<Func<string, bool>>(
        Expression.Call(typeof(Queryable), "Any", new Type[] { typeof(char) },
            Expression.Call(typeof(Queryable), "AsQueryable",
                            new Type[] { typeof(char) }, str),
            // !!!
            Expression.Constant(innerLambda)    // <--- !!!
        ),
        str
    );

// Works like a charm (prints one and three)
foreach (var str in array.AsQueryable().Where(expr))
    Console.WriteLine(str);

expr.ToString() 的输出对于两者也是相同的(无论我使用 Constant 还是 Quote ) .

鉴于上述观察结果,似乎 Expression.Quote() 是多余的 . 可以使用C#编译器将嵌套的lambda表达式编译成涉及 Expression.Constant() 而不是 Expression.Quote() 的表达式树,并且任何想要将表达式树处理成某种其他查询语言(例如SQL)的LINQ查询提供程序都可以查找 ConstantExpression 键入 Expression<TDelegate> 而不是具有特殊 Quote 节点类型的 UnaryExpression ,其他所有内容都相同 .

我错过了什么?为什么 Expression.Quote()UnaryExpression 的特殊 Quote 节点类型被发明了?

4 回答

  • 18

    简答:

    quote运算符是一个操作符,它在其操作数上引发闭包语义 . 常数只是值 .

    引号和常量具有不同的含义,因此在表达式树中具有不同的表示 . 对两个非常不同的事物具有相同的表示是非常混乱和容易出错的 .

    答案很长:

    考虑以下:

    (int s)=>(int t)=>s+t
    

    外部lambda是绑定到外部lambda参数的加法器的工厂 .

    现在,假设我们希望将其表示为稍后将被编译和执行的表达式树 . 表达树的主体应该是什么?这取决于您是否希望编译状态返回委托或表达式树 .

    让我们首先解雇这个无趣的案例 . 如果我们希望它返回一个委托,那么是否使用Quote或Constant的问题是一个有争议的问题:

    var ps = Expression.Parameter(typeof(int), "s");
            var pt = Expression.Parameter(typeof(int), "t");
            var ex1 = Expression.Lambda(
                    Expression.Lambda(
                        Expression.Add(ps, pt),
                    pt),
                ps);
    
            var f1a = (Func<int, Func<int, int>>) ex1.Compile();
            var f1b = f1a(100);
            Console.WriteLine(f1b(123));
    

    lambda有一个嵌套的lambda;编译器生成内部lambda作为一个函数的委托,该函数关闭为外部lambda生成的函数的状态 . 我们不再需要考虑这个案例了 .

    假设我们希望编译状态返回内部的表达式树 . 有两种方法可以做到:简单方法和艰难方式 .

    困难的方式是说而不是

    (int s)=>(int t)=>s+t
    

    我们真正的意思是

    (int s)=>Expression.Lambda(Expression.Add(...
    

    然后生成表达式树,产生这个混乱:

    Expression.Lambda(
                Expression.Call(typeof(Expression).GetMethod("Lambda", ...
    

    等等,等等几十行反射代码来制作lambda . quote运算符的目的是告诉表达式树编译器我们希望将给定的lambda视为表达式树而不是函数,而不必显式生成表达式树生成代码 .

    简单的方法是:

    var ex2 = Expression.Lambda(
                Expression.Quote(
                    Expression.Lambda(
                        Expression.Add(ps, pt),
                    pt)),
                ps);
    
            var f2a = (Func<int, Expression<Func<int, int>>>)ex2.Compile();
            var f2b = f2a(200).Compile();
            Console.WriteLine(f2b(123));
    

    事实上,如果您编译并运行此代码,您将得到正确的答案 .

    请注意,quote运算符是在内部lambda上引入闭包语义的运算符,它使用外部变量,外部lambda的形式参数 .

    问题是:为什么不消除Quote并让它做同样的事情?

    var ex3 = Expression.Lambda(
                Expression.Constant(
                    Expression.Lambda(
                        Expression.Add(ps, pt),
                    pt)),
                ps);
    
            var f3a = (Func<int, Expression<Func<int, int>>>)ex3.Compile();
            var f3b = f3a(300).Compile();
            Console.WriteLine(f3b(123));
    

    常量不会导致闭包语义 . 为什么要这样?你说这是一个常数 . 这只是一个 Value . 它应该是完美的交给编译器;编译器应该能够只在需要它的堆栈中生成该值的转储 .

    由于没有引起闭包,如果你这样做,你将得到一个'变量''类型'System.Int32'没有定义“调用异常 .

    (旁白:我刚刚从引用的表达式树中查看了代码生成的代码生成器,不幸的是,我在2006年放入代码的注释仍然存在 . 仅供参考,提升的外部参数在引用时被快照成常量表达式树由运行时编译器作为委托来实现 . 有一个很好的理由我编写代码的方式,我在这个时刻不记得,但它确实有引入闭包外部参数值的令人讨厌的副作用而不是关闭变量 . 显然,继承该代码的团队决定不修复该缺陷,所以如果你依赖在编译的引用内部lambda中观察到一个封闭的外部参数的变异,你会感到失望 . 但是,由于(1)改变形式参数和(2)依赖于外部变量的变异这是一个非常糟糕的编程习惯,我建议你改变你的程序,不要使用这两个糟糕的编程实践,而不是等待似乎没有即将到来的修复 . 为错误道歉 . )

    所以,重复一下这个问题:

    可以使用C#编译器将嵌套的lambda表达式编译为涉及Expression.Constant()而不是Expression.Quote()的表达式树,以及任何想要将表达式树处理成其他查询语言的LINQ查询提供程序(例如SQL)可以查找具有Expression类型的ConstantExpression而不是具有特殊Quote节点类型的UnaryExpression,其他所有内容都是相同的 .

    你是对的 . 我们可以使用常量表达式的类型作为标志来编码意味着"induce closure semantics on this value"的语义信息 .

    然后"Constant"将具有“使用此常量值”的含义,除非该类型恰好是表达式树类型且值是有效的表达式树,在这种情况下,请改为使用由重写内部结构而得到的表达式树的值 . 给定的表达式树在我们现在可能处于的任何外部lambda的上下文中引发闭包语义 .

    但为什么我们会那么疯狂呢?引用运算符是一个非常复杂的运算符,如果你为了简单地关于不在已经存在的几十个中添加一个额外的工厂方法和节点类型而应该明确地使用它,我们应该明确地使用它,我们添加了一个奇怪的角落案例常量,所以常量有时是逻辑常量,有时它们是带有闭包语义的重写lambda .

    它也有一些奇怪的效果,常数并不意味着"use this value" . 假设出于某些奇怪的原因,您希望上面的第三种情况将表达式树编译成一个委托,该委托分发一个表达式树,该表达式对外部变量有一个未重写的引用?为什么?也许是因为您正在测试编译器并希望只是传递常量,以便稍后可以对其执行一些其他分析 . 你的建议会让那不可能;任何恰好是表达式树类型的常量都将被重写 . 人们有一个合理的期望"constant"表示"use this value" . "Constant"是"do what I say"节点 . 恒定处理器的工作不是根据类型猜测你的意思 .

    当然,请注意,您现在正在承担理解的负担(即,理解常量具有复杂的语义,在一种情况下意味着"constant",并且"induce closure semantics"基于类型系统中的标志)在每个提供者进行语义分析时表达式树,而不仅仅是Microsoft提供商 . How many of those third-party providers would get it wrong?

    "Quote"正在挥舞着一个大红旗,上面写着"hey buddy, look over here, I'm a nested lambda expression and I have wacky semantics if I'm closed over an outer variable!"而"Constant"正在说"I'm nothing more than a value; use me as you see fit."当一些事情变得复杂和危险时,我们想让它挥动红旗,而不是通过让用户挖掘类型系统以找出是否隐藏这一事实这个值是否特殊 .

    此外,避免冗余甚至是目标的想法是不正确的 . 当然,避免不必要的,令人困惑的冗余是一个目标,但大多数冗余是一件好事;冗余创造了清晰度新的工厂方法和节点种类便宜 . 我们可以根据需要制作多个,以便每个人干净地代表一个操作 . 我们没有必要采取像"this means one thing unless this field is set to this thing, in which case it means something else."这样令人讨厌的技巧

  • -2

    这个问题已经得到了很好的答案 . 我还想指出一个资源,可以证明有关表达式树的问题:

    Microsoft有一个名为Dynamic Language Runtime的CodePlex项目 . 它的文档包括 Headers 为"Expression Trees v2 Spec"的文档,这正是:.NET 4中LINQ表达式树的规范 .

    例如,它说以下关于 Expression.Quote

    4.4.42引用在UnaryExpressions中使用引用来表示具有Expression类型的“常量”值的表达式 . 与Constant节点不同,Quote节点专门处理包含的ParameterExpression节点 . 如果包含的ParameterExpression节点声明将在结果表达式中关闭的本地,则为Quote替换其参考位置中的ParameterExpression . 在计算Quote节点的运行时,它将闭包变量引用替换为ParameterExpression引用节点,然后返回引用的表达式 . [...](第63-64页)

  • 0

    在这之后一个非常好的答案,它不是很清楚为什么它们是这样设计的,考虑:

    Expression.Lambda(Expression.Add(ps, pt));
    

    编译和调用此lambda时,它会计算内部表达式并返回结果 . 此处的内部表达式是一个加法,因此将评估 ps+pt 并返回结果 . 遵循此逻辑,以下表达式:

    Expression.Lambda(
        Expression.Lambda(
                  Expression.Add(ps, pt),
                pt), ps);
    

    应该在调用外部lambda时返回内部的lambda编译方法引用(因为我们说lambda编译为方法引用) . 那么为什么我们需要报价?!区分返回方法引用的情况与该引用调用的结果 .

    特别:

    let f = Func<...>
    return f; vs. return f(...);
    

    由于某种原因.Net设计师选择 Expression.Quote(f) 作为第一种情况,而普通 f 作为第二种情况 . 在我看来,这会造成很大的混乱,因为在大多数编程语言中返回值是直接的(不需要 Quote 或任何其他操作),但是调用确实需要额外的写入(括号参数),这转换为某种 invoke 在MSIL级别 . .Net设计师使其与表达树相反 . 知道原因会很有趣 .

  • 174

    我认为这里的重点是树的表现力 . 包含委托的常量表达式实际上只包含恰好是委托的对象 . 这比直接分解为一元二元表达式更具表现力 .

相关问题