代码之家  ›  专栏  ›  技术社区  ›  Timwi

是什么表达式.引号()这样做表达式.常量()不能已经做了吗?

  •  92
  • Timwi  · 技术社区  · 15 年前

    What is the purpose of LINQ's Expression.Quote method? 但是如果你继续读下去,你会发现它并没有回答我的问题。

    Expression.Quote() Expression.Constant() 可以用于相同的目的(除了 表达式.常量() 已用于)。因此,我不明白为什么 表达式.引号() 完全是必需的。

    为了证明这一点,我写了一个快速的例子,其中一个习惯性地使用 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() 两者也是一样的(无论我是否使用 常量 ).

    鉴于上述观察,看来 是多余的。C编译器可以将嵌套的lambda表达式编译成一个包含 表达式.常量() ,任何希望将表达式树处理为其他查询语言(如SQL)的LINQ查询提供程序都可以查找 ConstantExpression Expression<TDelegate> UnaryExpression 引用 节点类型,其他都是相同的。

    表达式.引号() 引用 一元表达式 发明?

    4 回复  |  直到 8 年前
        1
  •  194
  •   Eric Lippert    15 年前

    简短回答:

    引号运算符是 哪一个 在其操作数上归纳闭包语义

    引号和常量有不同的 意义 因此有 . 对两个完全不同的事物有相同的表示是

    长话短说:

    考虑以下几点:

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

    外lambda是绑定到外lambda参数的加法器的工厂。

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

            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。 引号运算符的目的是告诉表达式树编译器,我们希望将给定的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));
    

    常量不产生闭包语义。为什么要这么做?你说这是一个 常数 . 这只是一个价值观。交给编译器应该是完美的;编译器应该能够在需要的地方生成该值的转储。

    因为没有闭包,如果你这样做,你会得到一个“变量的类型”系统.Int32调用上的“未定义”异常。

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

    所以,重复这个问题:

    你说得对。我们 能够 使用常量表达式的类型作为标志

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

    但为什么呢 引号运算符是一个极其复杂的运算符 ,应该使用 如果你要用的话。你的意思是为了节省在已经存在的几十个方法中不添加一个额外的工厂方法和节点类型,我们给常量添加了一个奇怪的角大小写,所以常量有时是逻辑常量,有时用闭包语义重写lambda。

    通缉犯 您正在测试编译器 我们只想传递这个常数,这样以后就可以对它进行其他的分析了。你的提议将使这成为不可能;任何碰巧是表达式树类型的常量都将被重写。人们有一个合理的期望,“常数”意味着“使用这个值”常量是一个“照我说的做”节点。常量处理器的任务不是猜测你所做的事情 意味 根据类型来说。

    当然,请注意,您现在要承担理解的负担(也就是说,理解常量具有复杂的语义,在一种情况下表示“常量”,并基于 在类型系统中 对表达式树进行语义分析的提供者,而不仅仅是Microsoft提供者。 有多少第三方供应商会出错?

    “Quote”挥舞着一个大红旗,上面写着“嘿,伙计,看这里,我是一个嵌套的lambda表达式,如果我在一个外部变量上闭合的话,我有奇怪的语义!”而“Constant”的意思是“我不过是一个价值观,你觉得合适就用我吧。”当事情变得复杂和危险时,我们想让它飘扬红旗,而不是通过让用户挖掘数据来掩盖这个事实 以确定该值是否为特殊值。

    此外,避免冗余甚至是一个目标的想法是不正确的。当然,避免不必要的、令人困惑的冗余是一个目标,但大多数冗余都是一件好事;冗余创造了清晰。介绍了新的工厂方法和节点类型 . 我们可以根据需要制作多个,这样每一个都可以清晰地代表一个操作。我们没有必要求助于像“这意味着一件事,除非这个领域设置为这件事,在这种情况下,它意味着其他东西”这样的恶作剧

        2
  •  19
  •   neilco    6 年前

    在那里 微软的CodePlex项目 Dynamic Language Runtime . 其文件包括标题为, "Expression Trees v2 Spec"

    CodePlex不存在了。这个 Expression Trees v2 Spec (PDF) has moved to GitHub

    例如,它说关于 Expression.Quote

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

        3
  •  3
  •   Konstantin Triger    7 年前

    在这是一个非常好的答案之后,很清楚什么是语义。不太清楚 它们是这样设计的,请考虑:

    Expression.Lambda(Expression.Add(ps, pt));
    

    当编译和调用这个lambda时,它计算内部表达式并返回结果。这里的内部表达式是一个加法,所以 聚苯乙烯+铂 计算并返回结果。按照这个逻辑,下面的表达式:

    Expression.Lambda(
        Expression.Lambda(
                  Expression.Add(ps, pt),
                pt), ps);
    

    应该在调用外部lambda时返回内部的lambda编译的方法引用(因为我们说lambda编译为方法引用)。那我们为什么要报价?!区分方法引用返回时的情况与引用调用的结果。

    明确地:

    let f = Func<...>
    return f; vs. return f(...);
    

    表达式.引号(六) 在我看来,这会引起很大的混乱,因为在大多数编程语言中,返回值是直接的(不需要 引用 或任何其他操作),但调用确实需要额外的写入(括号+参数),这将转换为某种类型的 援引 在MSIL级别。Net的设计者们把表达式树做了相反的设计。我想知道原因。

        4
  •  0
  •   NetMage    5 年前

    我认为这更像是给予:

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

    你的树是 Expression.Lambda(Expression.Lambda) f Func<int> 那就回来了 2 .

    但如果你想要的是一只返回 表达式树 为了一只回来的羔羊 2 ,则需要:

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

    Expression.Lambda(Expression.Quote(Expression.Lambda)) Expression<Func<int>> 这是一个 那就回来了 2

        5
  •  -2
  •   Joe Wood    15 年前

    我想重点是这棵树的表现力。包含委托的常量表达式实际上只是包含一个碰巧是委托的对象。与直接分解为一元和二元表达式相比,它的表现力较弱。