在构建更复杂的表达式时如何重用表达式?

问题描述 投票:9回答:1

我正在努力学习表达,主要是为了我自己的教育。我正在尝试研究如何建立一个表达比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个回答
5
投票
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 => x / 2。 (顺便说一下,你也可以使用Expression.Divide()而不是MakeBinary更简洁一些。

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

此时你有一个代表Math.Sin(x)的表达式,你的下一步创建了lambda表达式x => 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

顺便说一句,在直接使用using static System.Linq.Expressions.Expression;类时,在文件中使用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 => Math.Sin(x/2),而是首先是sinex => Math.Sin(x),然后是第二个表达式x => sine(x / 2)

从概念上讲,这意味着你有两个已编译的lambda表达式,但编译器能够内联lambda,以便实际编译的内容再次回到x => Math.Sin(x/2),因此你没有两个独立编译的开销。

但更一般地说,考虑一下你的重用单位究竟是什么是值得的。如果我想生成几个在不同表达式结果上调用Math.Sin的表达式,我可能会继续使用MethodInfotypeof(Math).GetMethod("Sin")返回并将其用作我的可重用组件。

© www.soinside.com 2019 - 2024. All rights reserved.