技术开发 频道

表达式即编译器

  那么,什么是一个表达式树?

  与一般情况下C#编译得到的IL不同,一个Lambda表达式会被编译为一个表现代码逻辑的对象模型。于是,例如数据库提供者(provider)便可以观察这个对象模型,理解我们编写代码的目的,在必要时便可以将这种目的转化为其他形式(如T-SQL)。

  我们不妨独立观察先前的Lambda表达式:

Expression<Func<Customer, bool>> filter =
    cust
=> cust.Region == "North";

  如果要观察编译器做的事情,我们需要如平时般编译示例代码(Lambda表达式),然后在强大(且免费)的Reflector中查看——不过在此之前,我们要将.NET 3.5 (C# 3.0)优化选项关闭。

图1  关闭优化选项

  然后观察反编译器的输出内容,就会发现一些原本应该由我们自己编写的C#代码:

  请注意,虽然编译器直接访问了MemberInfo对象,并且使用了非法的变量名——所以你无法直接让这些输出编译通过,它只是用来参考的。有趣的是,事 实上C#语言规范中并没有指明编译器是如何将代码转化为表达式树的,因此,使用编译结果作为参考实现,是为数不多的可用于研究表达式树的方法之一。

  为了研究表达式树的手动编写方法,我们需要把相等性判断(==)和成员访问(.)视为接受运算对象的普通方法:

Expression<Func<Customer, bool>> filter =
    cust
=> Equal(Property(cust,"Region"),"North");

  我们现在可以构建一个相同的,接受Customer类型作为参数,判断其Region属性是否为字符串“North”,并返回一个布尔值的表达式树。

// declare a parameter of type Customer named cust

ParameterExpression cust
= Expression.Parameter(

    
typeof(Customer), "cust");

// compare (equality) the Region property of the// parameter against the string constant "North"

BinaryExpression body
= Expression.Equal(

    Expression.Property(cust,
"Region"),

    Expression.Constant(
"North", typeof(string)));

// formalise this as a lambda
Expression<Func<Customer, bool>> filter =

    Expression.Lambda
<Func<Customer, bool>>(body, cust);

  最后的“Lambda”语句是将表达式树标准化为一个完全独立的单元,并包含一系列(这个例子中只有一个)用于表达式树内部的参数。如果我们传入一个Lambda内不存在的参数,则会抛出一个异常。

  一个表达式树,其实只是一个表现我们意图的不可变的对象模型:

  • filter (lambda)
    • parameter: Customer: “cust”
    • body (binary)
      • method: equals
      • left (member)
        • member: “Region”
        • expression: “cust” parameter
      • right (constant)
        • string: “North”
  • 在获得一个完整的表达式树之后,我们可以将其交由LINQ提供者处理(此时它就会被真正当作一颗“树”来处理了),或者把它编译为一个委托——即动态生成所需的IL:

    Func<Customer, bool> filterFunc = filter.Compile();

      现在我们向委托对象中传入一个Customer实例并返回一个布尔值。

      这看上去有些麻烦,但已经远比使用Reflection.Emit的方式来的简单了,尤其在一些较为复杂的情况下(如装箱)。

      重要:将一个表达式树编译为委托对象涉及到动态代码的生成。如果想要获得非常好的的性能,你应该仅编译一次——将其储存起来(如放在一个字段中)并复用多次。

    0
    相关文章