简短回答:
引用操作符是一种 操作符,它在其操作数上 引入封闭语义。常量只是值。
引用和常量具有不同的 含义,因此在表达树中具有 不同的表示形式。对于两个非常不同的东西具有相同的表示形式是 极为 令人困惑和容易出错的。
详细回答:
考虑以下内容:
(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函数的状态。我们不需要再考虑这种情况。
假设我们希望编译后的状态返回内部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的形式参数。
问题是:为什么不消除引用操作符并使其执行相同的操作?
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年把一个注释放进了代码里。FYI,当引用表达式树被运行时编译器实例化为委托时,提升的外部参数会被快照为一个常量。我写代码的原因我此时并不记得了,但它确实有一个讨厌的副作用,即引入对外部参数的值而不是变量的闭包。显然,继承了那段代码的团队决定不修复这个缺陷,因此,如果您依赖于编译后引用的内部lambda中观察到的封闭外部参数的突变,您将会感到失望。但是,既然同时(1)突变形式参数和(2)依赖外部变量的突变是一种非常糟糕的编程实践,我建议您更改程序,不要使用这两个糟糕的编程实践,而不是等待一个看起来不会出现的修复。对错误表示歉意。)
那么,重复问题:
引用嵌套lambda表达式可以编译成涉及Expression.Constant()而不是Expression.Quote()的表达式树,任何想要将表达式树处理为其他查询语言(如SQL)的LINQ查询提供程序都可以查找类型为Expression的ConstantExpression而不是具有特殊Quote节点类型的UnaryExpression,其余所有内容都相同。
你是正确的。我们可以通过“使用常量表达式的类型作为标志”来编码意味着“在此值上引入闭包语义”,以便“常量”具有“使用此常量值”的含义,除非类型恰好是表达式树类型并且该值是有效的表达式树,在这种情况下,使用从重写给定表达式树的内部以在当前外部lambda的上下文中引入闭包语义的表达式树结果的值。
但是为什么我们要做这样疯狂的事情呢?引用运算符是一个极其复杂的运算符,并且应该明确地使用它。您建议为了不增加额外的工厂方法和节点类型而节俭,因此将奇怪的角落案例添加到常量中,以便有时常量在逻辑上是常量,有时它们是带有闭包语义的重写lambda。
它也会产生一些奇怪的影响,即常量并不意味着“使用这个值”。假设由于某种奇怪的原因,您希望第三种情况编译一个表达式树成为一个委托,该委托分发一个具有未重写对外部变量的引用的表达式树?为什么?也许是因为您正在测试编译器,并希望只需将常量传递下去,以便稍后执行其他一些分析。您的提议将使这种情况不可能发生;任何恰好是表达式树类型的常量都将被重写。人们有合理的期望,“常量”意味着“使用这个值”。 “常量”是一个“按我说的做”节点。常量处理器的工作不是根据类型猜测您的意图。
当然,请注意,您现在要承担理解的负担(也就是理解常量具有复杂的语义,在一种情况下是“常量”,在基于类型系统的标志引导下是“引入闭包语义”),不仅仅是微软提供者,而是每个进行表达式树语义分析的提供者。这些第三方提供者中有多少会出错呢?
“引用”正在挥舞着一个大红旗,上面写着“嘿伙计,看这里,我是一个嵌套的lambda表达式,如果我在外部变量上关闭,我的语义就很奇怪!”而“常量”则表示“我只是一个值;按您的意愿使用我。”当某些东西变得复杂和危险时,我们希望它挥舞着红旗,而不是通过使用户在类型系统中查找来隐藏这一事实,以便找出这个值是否是特殊值。
此外,避免冗余甚至是一个目标都是错误的。当然,避免不必要的、令人困惑的冗余是一个目标,但大多数冗余是有益的;冗余会产生清晰度。新的工厂方法和节点种类是廉价的。我们可以根据需要创建尽可能多的节点种类,以便每个节点种类都代表一个操作。我们没有必要采用像“这意味着一件事,除非将该字段设置为这个东西,否则它就意味着其他事情”的恶心技巧。