该Expression.Constant()无法做的Expression.Quote()有什么作用?


98

注意:我知道前面的问题“ 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);

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

鉴于以上观察,似乎Expression.Quote()是多余的。可以使C#编译器将嵌套的lambda表达式编译为一个包含Expression.Constant()而不是的表达式树Expression.Quote(),并且任何想要将表达式树处理为其他查询语言(例如SQL)的LINQ查询提供程序都可以查找ConstantExpressionwith类型Expression<TDelegate>而不是一个UnaryExpression用特殊Quote节点类型,以及其他一切将是相同的。

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

Answers:


189

简短答案:

该帖运算符是一个运营商,其诱导其操作关闭的语义。常数就是值。

引号和常量具有不同的含义,因此在表达式树中具有不同的表示形式。对于两种截然不同的事物,具有相同的表示形式是非常令人困惑和容易出错的。

长答案:

考虑以下:

(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使用外部变量(即外部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)依赖于外部变量的突变都是非常糟糕的编程习惯,所以我建议您更改程序以不使用这两种不好的编程习惯,而不是等待似乎不会出现的修复。对此错误表示歉意。)

因此,重复这个问题:

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

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

那么,“常数”的含义是“使用该常数值,除非该类型恰好是表达式树类型,并且该值是有效的表达式树,在这种情况下,请使用通过重写给定表达式树的内部,以在我们现在可能处于的任何外部lambda的上下文中引发闭包语义。

但为什么我们这样做疯狂的事?quote运算符是一个非常复杂的运算符,如果要使用它,则应明确使用它。您建议为了避免在已经存在的数十种方法中不添加任何额外的工厂方法和节点类型,我们在常量中添加了一个奇异的转角案例,以便常量有时在逻辑上是常量,而有时被重写具有关闭语义的lambda。

常量并不意味着“使用此值”也会有些奇怪。假设出于某种奇怪的原因,您希望上面的第三种情况将表达式树编译成一个委托,该委托分发了一个表达式树,该树具有对外部变量的未重写引用?为什么?也许是因为您正在测试编译器,并且只想传递常量,以便稍后可以对其进行其他分析。你的建议将使那不可能。无论如何,碰巧是表达式树类型的任何常量都将被重写。人们有一个合理的期望,即“常数”表示“使用此值”。“常量”是一个“按我说的做”节点。恒定处理器

并注意当然,你现在把理解的负担(即,理解是不断有复杂的语义,在一个案件平均“不变”和“诱导封闭语义”的基础上的标志是在类型系统)在不仅对Microsoft提供程序进行表达式树语义分析的提供程序。有多少第三方提供商会出错?

“ Quote”挥舞着一个大红色的标语,上面写着:“嘿,伙计,看看这里,我是一个嵌套的lambda表达式,如果我在一个外部变量上封闭的话,我的语言会很古怪!” 而“常数”则说“我不过是一种价值;请根据需要使用我。” 当某些事情变得复杂而危险时,我们希望使其成为红色标志,而不是通过使用户深入研究类型系统来发现该值是否为特殊值,从而隐藏该事实。

此外,避免冗余甚至是目标的想法是错误的。当然,避免不必要的,令人困惑的冗余是一个目标,但是大多数冗余是一件好事。冗余可以提高清晰度。新的工厂方法和节点类型很便宜。我们可以根据需要创建任意数量的对象,以便每个对象干净地代表一个操作。我们无需诉诸“这样的恶作剧”,例如“这意味着一件事,除非将此字段设置为该事物,在这种情况下,这意味着其他事情。”


11
我现在很尴尬,因为我没有想到闭包语义,也没有测试嵌套lambda从外部lambda捕获参数的情况。如果我这样做了,我会注意到其中的区别。再次非常感谢您的回答。
Timwi's

19

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

那里 是微软的CodePlex项目,名为 动态语言运行时。其文档包括标题为““表达树v2规范”,即:.NET 4中LINQ表达式树的规范。

更新: CodePlex已失效。该表达式树V2规格(PDF)已经转移到GitHub上

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

4.4.42报价

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


1
教人鱼的绝佳答案。我想补充一点,该文档已移动,现在可以在docs.microsoft.com/en-us/dotnet/framework/…上找到。引用的文件具体位于GitHub:github.com/IronLanguages/dlr/tree/master/Docs
relative_random

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级别调用。.Net设计师对表达式树则相反。知道原因会很有趣。


0

我相信它更像是给定的:

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

您的树是,Expression.Lambda(Expression.Lambda)并且f表示返回a的lambda的Expression Tree并Func<int>返回2

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

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

现在,您的树是,Expression.Lambda(Expression.Quote(Expression.Lambda))并且f表示返回a的lambda Expression<Func<int>>的表达式树,也Func<int>就是返回的a的表达式树2


-2

我认为这里的重点是树的表现力。包含委托的常量表达式实际上只是包含一个碰巧是委托的对象。这种表达方式不如直接分解为一元和二进制表达方式。


是吗?它到底增加了什么表现力?您可以用ConstantExpression无法表达的UnaryExpression(也使用一种奇怪的表达)来“表达”什么?
Timwi's
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.