【问题标题】:What does Expression.Quote() do that Expression.Constant() can’t already do?Expression.Quote() 做了哪些 Expression.Constant() 还不能做的事情?
【发布时间】:2011-04-12 14:28:37
【问题描述】:

注意:我知道之前的问题“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)的 L​​INQ 查询提供程序都可以查看对于具有 Expression&lt;TDelegate&gt; 类型的 ConstantExpression 而不是具有特殊 Quote 节点类型的 UnaryExpression,其他一切都相同。

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

【问题讨论】:

    标签: c# expression-trees


    【解决方案1】:

    简答:

    引号运算符是一个运算符,它在其操作数上引入闭包语义。常量只是值。

    引号和常量具有不同的含义,因此在表达式树中具有不同的表示。对两个非常不同的事物使用相同的表示非常令人困惑且容易出错。

    长答案:

    考虑以下几点:

    (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(...
    

    然后为that生成表达式树,产生this mess

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

    blah blah blah,几十行反射代码来制作 lambda。 引号运算符的目的是告诉表达式树编译器我们希望将给定的 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));
    

    事实上,如果你编译并运行这段代码,你会得到正确的答案。

    请注意,引号运算符是在内部 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));
    

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

    由于没有引发闭包,因此如果您这样做,您将在调用时收到“未定义类型为 'System.Int32' 的变量 's'”异常。

    (旁白:我刚刚查看了用于从引用的表达式树创建委托的代码生成器,不幸的是,我在 2006 年在代码中添加的注释仍然存在。仅供参考,提升的外部参数是 快照当引用的表达式树被运行时编译器具体化为委托时, 变成一个常量。我以这种方式编写代码是有充分理由的,我现在不记得了,但它确实有令人讨厌的在外部参数的上引入闭包而不是对变量的闭包的副作用。显然,继承该代码的团队决定不修复该缺陷,所以如果您依赖在编译的带引号的内部 lambda 中观察到封闭的外部参数的突变,你会失望的。但是,因为(1)改变形式参数和(2)依赖是一种非常糟糕的编程实践外部变量的突变,我建议你改变你的 prog ram 不要使用这两种糟糕的编程实践,而不是等待似乎不会出现的修复。为错误道歉。)

    所以,重复这个问题:

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

    你是对的。我们可以通过使用常量表达式的类型作为标志对语义信息进行编码,这意味着“在这个值上引入闭包语义”。

    “常量”的意思是“使用这个常量值,除非该类型恰好是表达式树类型并且该值是一个有效的表达式树,在这种情况下,请改用作为表达式树的值,该值是通过重写给定表达式树的内部而产生的,以在我们现在可能处于的任何外部 lambdas 的上下文中诱导闭包语义。

    但是为什么我们做那件疯狂的事呢? 引号运算符是一个极其复杂的运算符,如果你要使用它,应该明确地使用它。您的建议是,为了避免在已经存在的几十个工厂方法和节点类型中不添加一个额外的工厂方法和节点类型,我们为常量添加了一个奇怪的极端情况,以便常量有时是逻辑常量,有时它们会被重写具有闭包语义的 lambda。

    它也会产生一些奇怪的效果,即常量并不意味着“使用这个值”。假设出于某种奇怪的原因,您想要上面的第三种情况将表达式树编译成一个委托,该委托分发一个对外部变量具有未重写引用的表达式树?为什么?可能是因为您正在测试您的编译器,并且只想传递该常量,以便您稍后可以对其执行一些其他分析。你的提议将使这成为不可能;任何碰巧是表达式树类型的常量都将被重写。人们有一个合理的期望,即“常量”意味着“使用这个值”。 “常数”是一个“按我说的做”节点。常量处理器的工作不是根据类型猜测您的意思

    当然请注意,您现在承担了理解的负担(也就是说,理解常量具有复杂的语义,在一种情况下意味着“常量”和基于 in 的标志“诱导闭包语义”类型系统)在对表达式树进行语义分析的每个提供者上,而不仅仅是在微软提供者上。 这些第三方提供商中有多少会弄错?

    “报价”正在挥舞着一个大红旗,上面写着“嘿,伙计,看这里,我是一个嵌套的 lambda 表达式,如果我关闭了一个外部变量,我的语义很古怪!”而“常数”是说“我只不过是一个价值;在你认为合适的时候使用我。”当某件事复杂而危险时,我们希望让它挥动危险信号,而不是通过让用户挖掘类型系统来隐藏这个事实以找出这个值是否是一个特殊的值.

    此外,避免冗余甚至是一个目标的想法是不正确的。当然,避免不必要的、令人困惑的冗余是一个目标,但大多数冗余是一件好事。冗余创造清晰。新的工厂方法和节点类型便宜。我们可以根据需要制作任意数量的产品,以便每一个都干净地代表一个操作。我们没有必要诉诸诸如“这意味着一件事,除非这个字段被设置为这个东西,在这种情况下它意味着别的东西”之类的讨厌的技巧。

    【讨论】:

    • 我现在很尴尬,因为我没有想到闭包语义,也没有测试嵌套 lambda 从外部 lambda 捕获参数的情况。如果我这样做了,我会注意到差异。再次感谢您的回答。
    【解决方案2】:

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

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

    更新: CodePlex 已失效。 Expression Trees v2 Spec (PDF) has moved to GitHub

    例如,关于Expression.Quote的内容如下:

    4.4.42 引用

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

    【讨论】:

    【解决方案3】:

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

    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 或任何其他操作),但调用确实需要额外的写入(括号 + 参数) ,这会转化为 MSIL 级别的某种 invoke。 .Net 设计人员将其用于表达式树。知道原因会很有趣。

    【讨论】:

      【解决方案4】:

      我相信它更像是给定的:

      Expression<Func<Func<int>>> f = () => () => 2;
      

      您的树是 Expression.Lambda(Expression.Lambda)f 表示返回 Func&lt;int&gt; 的 lambda 表达式树,该 2 返回 2

      但是,如果您想要的是一个返回 表达式树 的 lambda,而 lambda 则返回 2,那么您需要:

      Expression<Func<Expression<Func<int>>>> f = () => () => 2;
      

      现在你的树是Expression.Lambda(Expression.Quote(Expression.Lambda))f 表示返回 Expression&lt;Func&lt;int&gt;&gt; 的 lambda 表达式树,即返回 Func&lt;int&gt; 的表达式树 2

      【讨论】:

        【解决方案5】:

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

        【讨论】:

        • 是吗?它究竟增加了什么表现力?你可以用那个 UnaryExpression(这也是一种奇怪的表达方式)“表达”你已经不能用 ConstantExpression 表达的什么?
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2012-07-26
        • 2017-03-05
        • 1970-01-01
        • 2019-09-11
        • 2017-07-13
        • 1970-01-01
        • 2019-11-08
        相关资源
        最近更新 更多