生成表达式树

C# 编译器创建了到目前为止看到的所有表达式树。 你创建了一个分配给类型为 Expression<Func<T>> 或某种类似类型的变量的 lambda 表达式。 对于许多方案,可以在运行时在内存中生成表达式。

表达式树是不可变的。 不可变意味着必须以从叶到根的方式生成表达式树。 用于生成表达式树的 API 反映了这一事实:用于生成节点的方法采用其所有子级作为参数。 让我们通过一些示例来向您展示这些技巧。

创建节点

首先,你将从这些章节中一直使用的加法表达式开始:

Expression<Func<int>> sum = () => 1 + 2;

若要构造该表达式树,首先构造叶节点。 叶节点是常量。 Constant使用该方法创建节点:

var one = Expression.Constant(1, typeof(int));
var two = Expression.Constant(2, typeof(int));

接下来,生成加法表达式:

var addition = Expression.Add(one, two);

生成加法表达式后,将创建 lambda 表达式:

var lambda = Expression.Lambda(addition);

此 lambda 表达式不包含任何参数。 在本部分的后面部分,你将了解如何将参数映射到参数并生成更复杂的表达式。

对于此类表达式,可以将所有调用合并到单个语句中:

var lambda2 = Expression.Lambda(
    Expression.Add(
        Expression.Constant(1, typeof(int)),
        Expression.Constant(2, typeof(int))
    )
);

生成树

上一部分显示了在内存中生成表达式树的基础知识。 更复杂的树通常意味着更多的节点类型,以及树中的更多节点。 让我们再运行一个示例,并显示创建表达式树时通常生成的两个节点类型:参数节点和方法调用节点。 让我们生成一个表达式树来创建此表达式:

Expression<Func<double, double, double>> distanceCalc =
    (x, y) => Math.Sqrt(x * x + y * y);

首先为xy创建参数表达式:

var xParameter = Expression.Parameter(typeof(double), "x");
var yParameter = Expression.Parameter(typeof(double), "y");

创建乘法和加法表达式遵循已看到的模式:

var xSquared = Expression.Multiply(xParameter, xParameter);
var ySquared = Expression.Multiply(yParameter, yParameter);
var sum = Expression.Add(xSquared, ySquared);

接下来,你需要为Math.Sqrt的调用创建一个方法调用表达式。

var sqrtMethod = typeof(Math).GetMethod("Sqrt", new[] { typeof(double) }) ?? throw new InvalidOperationException("Math.Sqrt not found!");
var distance = Expression.Call(sqrtMethod, sum);

如果未找到该方法,则 GetMethod 调用可能会返回 null 。 很可能是因为你拼错了方法名称。 否则,这可能意味着未加载所需的程序集。 最后,将方法调用放入 lambda 表达式中,并确保定义 lambda 表达式的参数:

var distanceLambda = Expression.Lambda(
    distance,
    xParameter,
    yParameter);

在这个更复杂的示例中,你将看到更多创建表达式树时常需使用的技术。

首先,需要在使用参数或局部变量之前创建表示参数或局部变量的对象。 创建这些对象后,可以根据需要在表达式树中使用这些对象。

其次,需要使用反射 API 的子集来创建对象 System.Reflection.MethodInfo ,以便可以创建表达式树来访问该方法。 您必须将自己限制在 .NET Core 平台上所提供的反射 API 子集之中。 同样,这些技术扩展到其他表达式树。

深度构建代码

使用这些 API 生成的内容不受限制。 但是,要生成的表达式树越复杂,代码就越难管理和读取。

让我们生成一个表达式树,该树等效于此代码:

Func<int, int> factorialFunc = (n) =>
{
    var res = 1;
    while (n > 1)
    {
        res = res * n;
        n--;
    }
    return res;
};

前面的代码没有构建表达式树,而只是创建了一个委托对象。 在使用Expression类时,无法构建语句 lambda 表达式。 下面是生成相同功能所需的代码。 没有用于生成 while 循环的 API,而是需要生成包含条件测试的循环,以及用于中断循环的标签目标。

var nArgument = Expression.Parameter(typeof(int), "n");
var result = Expression.Variable(typeof(int), "result");

// Creating a label that represents the return value
LabelTarget label = Expression.Label(typeof(int));

var initializeResult = Expression.Assign(result, Expression.Constant(1));

// This is the inner block that performs the multiplication,
// and decrements the value of 'n'
var block = Expression.Block(
    Expression.Assign(result,
        Expression.Multiply(result, nArgument)),
    Expression.PostDecrementAssign(nArgument)
);

// Creating a method body.
BlockExpression body = Expression.Block(
    new[] { result },
    initializeResult,
    Expression.Loop(
        Expression.IfThenElse(
            Expression.GreaterThan(nArgument, Expression.Constant(1)),
            block,
            Expression.Break(label, result)
        ),
        label
    )
);

构建阶乘函数表达式树的代码要长得多、复杂得多,而且充斥着标签、break 语句以及其他我们希望在日常编码任务中避免的元素。

在本部分中,你编写代码以访问此表达式树中的每个节点,并写出有关此示例中创建的节点的信息。 可以在 dotnet/docs GitHub 存储库 中查看或下载示例代码 。 通过生成和运行示例自行试验。

将代码构造映射到表达式

下面的代码示例演示了一个表达式树,该表达式树使用 API 表示 lambda 表达式 num => num < 5

// Manually build the expression tree for
// the lambda expression num => num < 5.
ParameterExpression numParam = Expression.Parameter(typeof(int), "num");
ConstantExpression five = Expression.Constant(5, typeof(int));
BinaryExpression numLessThanFive = Expression.LessThan(numParam, five);
Expression<Func<int, bool>> lambda1 =
    Expression.Lambda<Func<int, bool>>(
        numLessThanFive,
        new ParameterExpression[] { numParam });

表达式树 API 还支持分配和控制流表达式,例如循环、条件块和 try-catch 块。 通过使用 API,可以创建比 C# 编译器可从 lambda 表达式创建的表达式树更复杂的表达式树。 以下示例演示如何创建计算数字阶乘的表达式树。

// Creating a parameter expression.
ParameterExpression value = Expression.Parameter(typeof(int), "value");

// Creating an expression to hold a local variable.
ParameterExpression result = Expression.Parameter(typeof(int), "result");

// Creating a label to jump to from a loop.
LabelTarget label = Expression.Label(typeof(int));

// Creating a method body.
BlockExpression block = Expression.Block(
    // Adding a local variable.
    new[] { result },
    // Assigning a constant to a local variable: result = 1
    Expression.Assign(result, Expression.Constant(1)),
        // Adding a loop.
        Expression.Loop(
           // Adding a conditional block into the loop.
           Expression.IfThenElse(
               // Condition: value > 1
               Expression.GreaterThan(value, Expression.Constant(1)),
               // If true: result *= value --
               Expression.MultiplyAssign(result,
                   Expression.PostDecrementAssign(value)),
               // If false, exit the loop and go to the label.
               Expression.Break(label, result)
           ),
       // Label to jump to.
       label
    )
);

// Compile and execute an expression tree.
int factorial = Expression.Lambda<Func<int, int>>(block, value).Compile()(5);

Console.WriteLine(factorial);
// Prints 120.

有关详细信息,请参阅 在 Visual Studio 2010 中使用表达式树生成动态方法,这也适用于更高版本的 Visual Studio。