C#3.0新增功能10 表达式树 04 执行表达式

表达式树 是表示一些代码的数据结构。 它不是已编译且可执行的代码。 若是想要执行由表达式树表示的 .NET 代码,则必须将其转换为可执行的 IL 指令。html

Lambda 表达式到函数

能够将任何 LambdaExpression 或派生自 LambdaExpression 的任何类型转换为可执行的 IL。 其余表达式类型不能直接转换为代码。 此限制在实践中影响不大。 Lambda 表达式是你可经过转换为可执行的中间语言 (IL) 来执行的惟一表达式类型。 (思考直接执行 ConstantExpression 意味着什么。 这是否意味着任何用处?)LambdaExpression 或派生自 LambdaExpression 的类型的任何表达式树都可转换为 IL。 表达式类型 Expression<TDelegate> 是 .NET Core 库中的惟一具体示例。 它用于表示映射到任何委托类型的表达式。 因为此类型映射到一个委托类型,所以 .NET 能够检查表达式,并为匹配 lambda 表达式签名的适当委托生成 IL。算法

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

LambdaExpression 类型包含用于将表达式树转换为可执行代码的 Compile 和 CompileToMethod成员。 Compile 方法建立委托。 CompileToMethod 方法经过表示表达式树的已编译输出的 IL 更新 MethodBuilder 对象。 请注意,CompileToMethod 仅在完整的桌面框架中可用,不能用于 .NET Core。缓存

还能够选择性地提供 DebugInfoGenerator,它将接收生成的委托对象的符号调试信息。 这让你能够将表达式树转换为委托对象,并拥有生成的委托的完整调试信息。数据结构

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

Expression<Func<int>> add = () => 1 + 2;
var func = add.Compile(); // 生产委托
var answer = func();      // 执行委托
Console.WriteLine(answer);

请注意,该委托类型基于表达式类型。 若是想要以强类型的方式使用委托对象,则必须知道返回类型和参数列表。 LambdaExpression.Compile() 方法返回 Delegate 类型。 必须将其转换为正确的委托类型,以便使任何编译时工具检查参数列表或返回类型。框架

执行和生存期

经过调用在调用 LambdaExpression.Compile() 时建立的委托来执行代码。 能够在上面进行查看,其中 add.Compile() 返回了一个委托。 经过调用 func() 调用该委托将执行代码。函数

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

在此提醒你不要经过避免没必要要的编译调用尝试建立用于提升性能的任何更复杂的缓存机制。 比较两个任意的表达式树,以肯定若是它们表示相同的算法,是否也会花费很长的时间来执行。 你可能会发现,经过避免对 LambdaExpression.Compile() 的任何额外调用所节省的计算时间将多于执行代码(该代码肯定可致使相同可执行代码的两个不一样表达式树)所花费的时间性能

注意事项

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

Lambda 表达式将对表达式中引用的任何局部变量建立闭包。 必须保证做为委托的一部分的任何变量在调用 Compile 的位置处和执行结果委托时可用。

通常状况下,编译器会确保这一点。 可是,若是表达式访问实现 IDisposable 的变量,则代码可能在表达式树仍保留有对象时释放该对象。

例如,此代码工做正常,由于 int 不实现 IDisposable

private static Func<int, int> CreateBoundFunc()
{
    var constant = 5; // 常量由表达式树捕获
    Expression<Func<int, int>> expression = (b) => constant + b;
    var rVal = expression.Compile();
    return rVal;
}

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

可是,请考虑实现 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 属性引用的代码时将出现 ObjectDisposedException

private static Func<int, int> CreateBoundResource()
{
    using (var constant = new Resource()) // 常量由表达式树捕获
    {
        Expression<Func<int, int>> expression = (b) => constant.Argument + b;
        var rVal = expression.Compile();
        return rVal;
    }
}

今后方法返回的委托已对释放了的 constant 对象闭包。 (它已被释放,由于它已在 using 语句中进行声明。)

如今,在执行今后方法返回的委托时,将在执行时引起 ObjectDisposedException

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

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

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

总结

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

表达式树表示会为建立的任意给定构造执行的代码。 只要编译和执行代码的环境匹配建立表达式的环境,则一切将按预期进行。 若是未按预期进行,那么错误也是很容易预知的,而且将在使用表达式树的任何代码的第一个测试中捕获这些错误。

 

相关文章
相关标签/搜索