简答:
引号运算符是一个运算符,它在其操作数上引入闭包语义。常量只是值。
引号和常量具有不同的含义,因此在表达式树中具有不同的表示。对两个非常不同的事物使用相同的表示非常令人困惑且容易出错。
长答案:
考虑以下几点:
(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 表达式,如果我关闭了一个外部变量,我的语义很古怪!”而“常数”是说“我只不过是一个价值;在你认为合适的时候使用我。”当某件事复杂而危险时,我们希望让它挥动危险信号,而不是通过让用户挖掘类型系统来隐藏这个事实以找出这个值是否是一个特殊的值.
此外,避免冗余甚至是一个目标的想法是不正确的。当然,避免不必要的、令人困惑的冗余是一个目标,但大多数冗余是一件好事。冗余创造清晰。新的工厂方法和节点类型便宜。我们可以根据需要制作任意数量的产品,以便每一个都干净地代表一个操作。我们没有必要诉诸诸如“这意味着一件事,除非这个字段被设置为这个东西,在这种情况下它意味着别的东西”之类的讨厌的技巧。