【问题标题】:How do I reuse an Expression when building a more complex one?在构建更复杂的表达式时如何重用表达式?
【发布时间】:2017-12-21 15:48:04
【问题描述】:

我正在尝试学习表达式,主要是为了我自己的教育。我正在尝试研究如何构建一个表达式,它可以表示比a+b 等更复杂的东西。

我将逐步完成此操作,以便您了解我是如何构建它的。请随时对我的方法的任何方面发表评论,尽管实际问题出现在第三个代码块上。

我了解如何制作一个将输入除以 2 的函数:

// Set up a parameter for use below
ParameterExpression x = Expression.Parameter(typeof(double), "x");

Expression two = Expression.Constant((double)2);
Expression halve = Expression.MakeBinary(ExpressionType.Divide, x, two);
// Test it
double halfOfTwenty = Expression.Lambda<Func<double, double>>(halve, x).Compile()(20);

我还想出了如何制作一个计算 sin(x) 的表达式:

Expression sine = Expression.Call(typeof(Math).GetMethod("Sin"), x);
// Test it
double sinePiOverTwo = Expression.Lambda<Func<double, double>>(sine, x).Compile()(Math.PI / 2);

我现在想做的是创建一个计算 sin(x/2) 的表达式。我可以这样...

Expression sineOfHalf = Expression.Call(typeof(Math).GetMethod("Sin"), halve);

...但理想情况下,我想重用我现有的正弦表达式,而不是创建一个新的。

我确信这很简单,但对于这个领域的新手来说,我发现它相当困难。任何人都可以告诉我如何做到这一点?我查看了 Expression 类,但显然忽略了我需要的方法。

【问题讨论】:

    标签: c# lambda expression-trees


    【解决方案1】:
    ParameterExpression x = Expression.Parameter(typeof(double), "x");
    
    Expression two = Expression.Constant((double)2);
    Expression halve = Expression.MakeBinary(ExpressionType.Divide, x, two);
    

    好的,此时您有代表x / 2 的内容,下一步创建了 lambda 表达式x =&gt; x / 2。 (顺便说一句,您也可以使用 Expression.Divide() 而不是 MakeBinary 更简洁一些。

    Expression sine = Expression.Call(typeof(Math).GetMethod("Sin"), x);
    

    此时,您有一个表示 Math.Sin(x) 的表达式,下一步创建了 lambda 表达式 x =&gt; Math.Sin(x)

    所以你需要做的是把你在每次创建 lambda 表达式之前的两点结合起来:

    Expression sine = Expression.Call(typeof(Math).GetMethod("Sin"), halve);
    

    现在你可以做最后一步了:

    Expression.Lambda<Func<double, double>>(sine, x) // x => Math.Sin(x / 2.0)
    

    完整代码:

    ParameterExpression x = Expression.Parameter(typeof(double), "x");
    Expression two = Expression.Constant((double)2);
    Expression halve = Expression.MakeBinary(ExpressionType.Divide, x, two);
    Expression sine = Expression.Call(typeof(Math).GetMethod("Sin"), halve);
    Expression<Func<double, double>> sineHalveLambda = Expression.Lambda<Func<double, double>>(sine, x);
    

    然后进行测试:

    Func<double, double> f = sineHalveLambda.Compile();
    Console.WriteLine(f(Math.PI));  // 1
    Console.WriteLine(f(0));        // 0
    Console.WriteLine(f(-Math.PI)); // -1
    

    顺便说一下,当直接使用 Expression 类时,在文件中包含 using static System.Linq.Expressions.Expression; 通常很有用,因为您会经常使用它的静态成员,然后它有时 如果您将其作为单行但缩进反映树,则有助于可视化生成的树:

    ParameterExpression x = Parameter(typeof(double), "x");
    Expression<Func<double, double>> sineHalveLambda = Lambda<Func<double, double>>(
        Call(
            typeof(Math).GetMethod("Sin"),
            Divide(
                x,
                Constant(2.0)
            )
        )
        , x);
    

    因为缩进反映了生成的表达式树的分支。虽然在反映树结构的可读性优势与一般单行代码的一般可读性劣势之间存在平衡。

    编辑:正如@Evk 指出的那样,我错过了你的问题中说“我可以像这样......”这与上面差不多。

    要实际重用sine 表达式,有几种可能的方法。

    您可以使用Update,它会根据您正在使用的Expression 生成Expression,并带有不同的孩子。这在ExpressionVisitors 中被大量使用。

    您还可以创建一个 lambda 表达式并在另一个表达式中调用该 lambda:

    ParameterExpression x = Expression.Parameter(typeof(double), "x");
    Expression two = Expression.Constant((double)2);
    Expression halve = Expression.MakeBinary(ExpressionType.Divide, x, two);
    Expression sine = Expression.Call(typeof(Math).GetMethod("Sin"), x);
    Expression sineLambda = Expression.Lambda<Func<double, double>>(sine, x);
    Expression<Func<double, double>> sineHalfLambda = Expression.Lambda<Func<double, double>>(Expression.Invoke(sineLambda, halve), x);
    Func<double, double> sineHalfDelegate = sineHalfLambda.Compile();
    

    您实际生成的不是x =&gt; Math.Sin(x/2),而是首先是sine,即x =&gt; Math.Sin(x),然后是第二个表达式,即x =&gt; sine(x / 2)

    从概念上讲,这意味着您有两个已编译的 lambda 表达式,但编译器能够内联内部 lambda,以便实际编译的内容再次恢复为 x =&gt; Math.Sin(x/2),因此您没有开销两个单独的编译。

    不过,更一般地说,考虑一下您的重用单元到底是什么是值得的。如果我想针对不同表达式的结果生成多个调用 Math.Sin 的表达式,我可能会保留 typeof(Math).GetMethod("Sin") 返回的 MethodInfo 并将其用作我的可重用组件。

    【讨论】:

    • 太棒了,谢谢!这东西很聪明,只是有点难学:)
    • @AvrohomYisroel 但与您自己在“我可以这样做......”部分中提到的不完全相同吗?
    • 值得在LinqPad 中用表达式语法Expression&lt;Func&lt;double, double&gt;&gt; e = x =&gt; Math.Sin(x / 2); 编写一个表达式,然后调用e.Dump(),这样它可以让您了解模型并了解编译器是如何构建它的,尽管有时编译器可能不会手动执行一些步骤,有些事情您可以手动执行编译器不会,它会提供一些关于如何构建更复杂表达式的见解。
    • @JonHanna 我一直在这样做,这很有启发性,但它并没有告诉我要调用 Expression 类中的哪些方法来自己构建表达式。这就是我卡住的地方。顺便说一句,在@Evk 的评论之后,我再次查看了您的代码,您命名为sine 的表达式实际上应该命名为sineOfHalf(或类似名称),并且确实是我发布的内容。在创建计算 sin(x/2) 的表达式时,有没有办法重用我原来的 sine 表达式?
    • @AvrohomYisroel 你可以像这样重用你的正弦表达式:var sineOfHalf = sine.Update(sine.Object, new[] {halve});(注意UpdateMethodCallExpression的方法,所以把Expression sine = ...改成var sine = ...)。这会将当前表达式的参数(“x”)替换为您传递的参数(表示 x\2)并返回更新后的表达式(原始表达式将保持不变,因为表达式是不可变的)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多