gpt4 book ai didi

c# - Expression.Quote() 能做哪些 Expression.Constant() 不能做的事?

转载 作者:行者123 更新时间:2023-12-01 17:42:03 29 4
gpt4 key购买 nike

注意:我知道之前的问题“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 查询提供程序都可以查找 ConstantExpression带类型 Expression<TDelegate>而不是 UnaryExpression与特别 Quote节点类型,其他一切都一样。

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

最佳答案

简短的回答:

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

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

长答案:

请考虑以下事项:

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

外部 lambda 是绑定(bind)到外部 lambda 参数的加法器的工厂。

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

让我们首先驳回这个无趣的案例。如果我们希望它返回一个委托(delegate),那么是使用 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 作为函数的委托(delegate),该函数在为外部 lambda 生成的函数状态上关闭。我们不需要再考虑这种情况了。

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

困难的方法是说,而不是
(int s)=>(int t)=>s+t

我们真正的意思是
(int s)=>Expression.Lambda(Expression.Add(...

然后为此生成表达式树,产生这样的困惑:
        Expression.Lambda(
Expression.Call(typeof(Expression).GetMethod("Lambda", ...

等等等等,几十行反射代码来制作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'”异常。

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

所以,重复这个问题:

The C# compiler could have been made to compile nested lambda expressions into an expression tree involving Expression.Constant() instead of Expression.Quote(), and any LINQ query provider that wants to process expression trees into some other query language (such as SQL) could look out for a ConstantExpression with type Expression instead of a UnaryExpression with the special Quote node type, and everything else would be the same.



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

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

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

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

当然,请注意,您现在将理解负担(即,理解常量具有复杂的语义,在一种情况下意味着“常量”,并且基于类型系统中的标志“诱导闭包语义”)在每个对表达式树进行语义分析的提供程序,而不仅仅是对 Microsoft 提供程序进行分析。 有多少第三方供应商会弄错?

“Quote” 挥舞着一面大红旗,上面写着“嘿,伙计,看这里,我是一个嵌套的 lambda 表达式,如果我关闭了一个外部变量,我就会有古怪的语义!”而“Constant”则是在说“我只不过是一个值(value);按照你认为合适的方式使用我。”当某些事情复杂和危险时,我们希望让它发出危险信号,而不是通过让用户挖掘类型系统以找出该值是否特殊来隐藏这一事实。

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

关于c# - Expression.Quote() 能做哪些 Expression.Constant() 不能做的事?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/3716492/

29 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com