执行表达式树

表达式树是表示一些代码的数据结构。 它不是经过编译且可执行的代码。 如果想要执行由表达式树表示的 .NET 代码,必须将其转换为可执行的 IL 指令。 执行表达式树可能返回一个值,或者它可能只是执行操作,例如调用方法。

仅可以执行表示 lambda 表达式的表达式树。 表示 Lambda 表达式的表达式树的类型为 LambdaExpressionExpression<TDelegate>。 若要执行这些表达式树,请调用 Compile 方法来创建一个可执行的委托,然后调用该委托。

注意

如果委托的类型未知,也就是说 Lambda 表达式的类型为 LambdaExpression 而不是 Expression<TDelegate>,则对委托调用 DynamicInvoke 方法,而不是直接调用委托。

如果表达式树不表示 Lambda 表达式,可以通过调用 Lambda<TDelegate>(Expression, IEnumerable<ParameterExpression>) 方法创建一个新的 Lambda 表达式,此表达式的主体为原始表达式树。 然后,按本节前面所述执行此 lambda 表达式。

Lambda 表达式到函数

可以将任何 LambdaExpression 或派生自 LambdaExpression 的任何类型转换为可执行的 IL。 其他表达式类型不能直接转换为代码。 此限制在实践中影响不大。 Lambda 表达式是你可通过转换为可执行的中间语言 (IL) 来执行的唯一表达式类型。 (思考直接执行 System.Linq.Expressions.ConstantExpression 意味着什么。这是否意味着任何用处?)System.Linq.Expressions.LambdaExpression 或派生自 LambdaExpression 的类型的任何表达式树均可转换为 IL。 表达式类型 System.Linq.Expressions.Expression<TDelegate> 是 .NET Core 库中的唯一具体示例。 它用于表示映射到任何委托类型的表达式。 由于此类型映射到一个委托类型,因此 .NET 可以检查表达式,并为匹配 lambda 表达式签名的适当委托生成 IL。 该委托类型基于表达式类型。 如果想要以强类型的方式使用委托对象,则必须知道返回类型和参数列表。 LambdaExpression.Compile() 方法返回 Delegate 类型。 必须将它强制转换为正确的委托类型,以便使任何编译时工具检查参数列表或返回类型。

在大多数情况下,表达式和其对应的委托之间存在简单映射。 例如,由 Expression<Func<int>> 表示的表达式树将被转换为 Func<int> 类型的委托。 对于具有任何返回类型和参数列表的 Lambda 表达式,存在这样的委托类型:该类型是由该 Lambda 表达式表示的可执行代码的目标类型。

System.Linq.Expressions.LambdaExpression 类型包含用于将表达式树转换为可执行代码的 LambdaExpression.CompileLambdaExpression.CompileToMethod 成员。 Compile 方法创建委托。 CompileToMethod 方法通过表示表达式树的已编译输出的 IL 更新 System.Reflection.Emit.MethodBuilder 对象。

重要

CompileToMethod 仅在 .NET Framework 中可用,在 .NET Core 或 .NET 5 及更高版本中不可用。

还可以选择性地提供 System.Runtime.CompilerServices.DebugInfoGenerator,它接收生成的委托对象的符号调试信息。 DebugInfoGenerator 提供有关生成的委托的完整调试信息。

使用下面的代码将表达式转换为委托:

Expression<Func<int>> add = () => 1 + 2;
var func = add.Compile(); // Create Delegate
var answer = func(); // Invoke Delegate
Console.WriteLine(answer);

下面的代码示例演示了在编译和执行表达式树时使用的具体类型。

Expression<Func<int, bool>> expr = num => num < 5;

// Compiling the expression tree into a delegate.
Func<int, bool> result = expr.Compile();

// Invoking the delegate and writing the result to the console.
Console.WriteLine(result(4));

// Prints True.

// You can also use simplified syntax
// to compile and run an expression tree.
// The following line can replace two previous statements.
Console.WriteLine(expr.Compile()(4));

// Also prints True.

下面的代码示例演示如何通过创建 lambda 表达式并执行它来执行代表幂运算的表达式树。 示例最后显示幂运算的结果。

// The expression tree to execute.
BinaryExpression be = Expression.Power(Expression.Constant(2d), Expression.Constant(3d));

// Create a lambda expression.
Expression<Func<double>> le = Expression.Lambda<Func<double>>(be);

// Compile the lambda expression.
Func<double> compiledExpression = le.Compile();

// Execute the lambda expression.
double result = compiledExpression();

// Display the result.
Console.WriteLine(result);

// This code produces the following output:
// 8

执行和生存期

通过调用在调用 LambdaExpression.Compile() 时创建的委托来执行代码。 上述代码 add.Compile() 返回委托。 可以通过调用 func() 来调用该委托,它将执行代码。

该委托表示表达式树中的代码。 可以保留该委托的句柄并在稍后调用它。 不需要在每次想要执行表达式树所表示的代码时编译表达式树。 (请记住,表达式树是不可变的,并且在之后编译同一表达式树会创建执行相同代码的委托。)

注意

不要通过避免不必要的编译调用来创建用于提高性能的任何更复杂的缓存机制。 比较两个任意的表达式树,确定如果它们表示相同的算法,是否也是一项耗时的操作。 通过避免对 LambdaExpression.Compile() 的任何额外调用所节省的计算时间可能多于执行代码(该代码用于确定两个不同的表达式树是否可产生相同的可执行代码)所花费的时间。

注意事项

将 lambda 表达式编译为委托并调用该委托是可对表达式树执行的最简单的操作之一。 但是,即使是执行这个简单的操作,也存在一些必须注意的事项。

Lambda 表达式将对表达式中引用的任何局部变量创建闭包。 必须保证作为委托的一部分的任何变量在调用 Compile 的位置处和执行结果委托时可用。 编译器确保变量处于范围内。 但是,如果表达式访问用于实现 IDisposable 的变量,则代码可能在表达式树仍保留有对象时释放该对象。

例如,此代码正常工作,因为 int 不实现 IDisposable

private static Func<int, int> CreateBoundFunc()
{
    var constant = 5; // constant is captured by the expression tree
    Expression<Func<int, int>> expression = (b) => constant + b;
    var rVal = expression.Compile();
    return rVal;
}

委托已捕获对局部变量 constant 的引用。 在稍后执行 CreateBoundFunc 返回的函数之后,可随时访问该变量。

但是,请考虑可实现 System.IDisposable 的以下(人为设计的)类:

public class Resource : IDisposable
{
    private bool _isDisposed = false;
    public int Argument
    {
        get
        {
            if (!_isDisposed)
                return 5;
            else throw new ObjectDisposedException("Resource");
        }
    }

    public void Dispose()
    {
        _isDisposed = true;
    }
}

如果将它用于以下代码中所示的表达式中,则在执行 Resource.Argument 属性引用的代码时将出现 System.ObjectDisposedException

private static Func<int, int> CreateBoundResource()
{
    using (var constant = new Resource()) // constant is captured by the expression tree
    {
        Expression<Func<int, int>> expression = (b) => constant.Argument + b;
        var rVal = expression.Compile();
        return rVal;
    }
}

从此方法返回的委托已对释放了的 constant 对象闭包。 (它已被释放,因为它已在 using 语句中进行声明。)

现在,在执行从此方法返回的委托时,将在执行时引发 ObjectDisposedException

出现表示编译时构造的运行时错误确实很奇怪,但这是使用表达式树时的正常现象。

此问题存在大量的排列,因此很难提供用于避免此问题的一般性指导。 定义表达式时,请谨慎访问局部变量,并且在创建通过公共 API 返回的表达式树时,谨慎访问当前对象(由 this 表示)中的状态。

表达式中的代码可能引用其他程序集中的方法或属性。 对表达式进行定义、编译或在调用生成的委托时,该程序集必须可访问。 在它不存在的情况下,将遇到 ReferencedAssemblyNotFoundException

总结

可以编译表示 lambda 表达式的表达式树,以创建可执行的委托。 表达式树提供了一种机制,用于执行表达式树所表示的代码。

表达式树表示会为创建的任意给定构造执行的代码。 只要编译和执行代码的环境匹配创建表达式的环境,则一切将按预期进行。 如果未按预期进行,那么错误是可以预知的,并且将在使用表达式树的任何代码的第一批测试中捕获这些错误。