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

Tim*_*mwi 93 c# expression-trees

注意:我知道之前的问题" LINQ的Expression.Quote方法的目的是什么?",但如果你继续阅读,你会发现它没有回答我的问题.

我明白所说的目的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);
Run Code Online (Sandbox Code Playgroud)

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

鉴于上述观察结果,似乎Expression.Quote()是多余的.本来可以进行的C#编译器编译嵌套Lambda表达式到表达式树涉及Expression.Constant()代替Expression.Quote(),而且要处理表达式树到一些其他的查询语言(如SQL)任何LINQ查询供应商可以看出来的一个ConstantExpression类型Expression<TDelegate>,而不是a UnaryExpression具有特殊Quote节点类型,其他所有内容都是相同的.

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

Eri*_*ert 184

简短回答:

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

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

答案很长:

考虑以下:

(int s)=>(int t)=>s+t
Run Code Online (Sandbox Code Playgroud)

外部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));
Run Code Online (Sandbox Code Playgroud)

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

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

困难的方式是说而不是

(int s)=>(int t)=>s+t
Run Code Online (Sandbox Code Playgroud)

我们真正的意思是

(int s)=>Expression.Lambda(Expression.Add(...
Run Code Online (Sandbox Code Playgroud)

然后生成的表达式树认为,产生这个烂摊子:

        Expression.Lambda(
            Expression.Call(typeof(Expression).GetMethod("Lambda", ...
Run Code Online (Sandbox Code Playgroud)

等等,等等几十行反射代码来制作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));
Run Code Online (Sandbox Code Playgroud)

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

请注意,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));
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

你是对的.我们可以通过使用常量表达式的类型作为标志编码语义信息,这意味着"在此值上引发闭包语义" .

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

但为什么我们这样做疯狂的事?引号运算符是一个非常复杂的运算符,如果要使用它,应该明确使用它.你建议为了简单地在几十个已经存在的情况下不添加一个额外的工厂方法和节点类型,我们在常量中添加了一个奇怪的角点情况,因此常量有时是逻辑常量,有时它们会被重写具有闭包语义的lambdas.

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

并注意当然,你现在把认识的负担(即理解是不断有复杂的,在一个案例中的意思是"不变"和"诱导封闭语义"的基础上的标记是语义的类型系统)在对表达式树进行语义分析的提供程序,而不仅仅是Microsoft提供程序.有多少第三方提供商会弄错?

"引用"正在挥动一个大红旗,上面写着"嘿伙计,看看这里,我是一个嵌套的lambda表达式,如果我关闭了外部变量,我就会有古怪的语义!" 而"常数"则说"我只不过是一种价值;在你认为合适时使用我." 当某些东西变得复杂和危险时,我们想让它挥动红旗,而不是通过让用户挖掘类型系统来隐藏这个事实,以便找出这个值是否是特殊值.

此外,避免冗余甚至是目标的想法是不正确的.当然,避免不必要的,令人困惑的冗余是一个目标,但大多数冗余是一件好事; 冗余创造了清晰度 新的工厂方法和节点种类便宜.我们可以根据需要制作多个,以便每个人干净地代表一个操作.我们没有必要采取令人讨厌的技巧,"这意味着一件事,除非这个领域设置为这个东西,在这种情况下,它意味着别的东西."

  • 我现在很尴尬,因为我没有想到闭包语义,也没能测试嵌套的lambda从外部lambda捕获参数的情况.如果我这样做了,我会注意到差异.非常感谢你的回答. (11认同)

sta*_*ica 19

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

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

例如,它说以下内容Expression.Quote:

4.4.42报价

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