LINQ与DLR的Expression tree(1):简介LINQ与Expression tree

以前在这边也发过关于LINQ的帖,不过并没有涉及比较深入的部分。之前的这篇:使用lambda expression来动态获取delegate,然后用Cecil注入代码(1)并不是没有下文了,而是在短短的时间内情况发生了许多变化,使得一些原本的想法有机会以更简单的方式来完成。接下来的一段时间我会发一些与LINQ、DLR等相关的帖,探讨它们与动态代码生成、动态类型代码等的关系,也将会继续讨论通过它们来进行依赖注入的话题。 

关于这个“树”的叫法……LINQ里的表达式树有官方文档就叫Expression tree,但DLR中相应的语法树却没有什么固定的正式叫法,有时候叫做DLR AST,有时候叫DLR tree。本文(以及接下来的文章)都将使用DLR tree来称呼DLR中的语法树。 

LINQ与Expression tree 

那么,先简单的说明LINQ与Expression tree的关系。LINQ(Language-INtegrated Query)是.NET Framework 3.5新引入的技术,使得对数据集合的操作能够以统一的API实现。其中,.NET Framework自带的LINQ实现有LINQ-to-Objects、LINQ-to-DataSet、LINQ-to-SQL、LINQ-to-XML等实现。它依赖于.NET Framework 3.5引入的另外几项技术,包括扩展方法(extension method)、lambda表达式(lambda expression)、匿名类型(anonymous type)、自动实现的属性(auto-implemented property)、局部变量类型推导(local variable type inference)等。表达式树(Expression tree)则是LINQ本身的重要组成部分之一。 

引用中文MSDN上的文档: 

引用
语言集成查询 (LINQ) 
LINQ 中的表达式目录树 
在 LINQ 中,表达式目录树用于表示针对数据源的结构化查询,这些数据源实现 IQueryable<T>。例如,LINQ to SQL 提供程序实现 IQueryable<T> 接口,用于查询关系数据存储。C# 和 Visual Basic 编译器会将针对此类数据源的查询编译为代码,该代码在运行时将生成一个表达式目录树。然后,查询提供程序可以遍历表达式目录树数据结构,并将其转换为适合于数据源的查询语言。 

表达式目录树还可以用在 LINQ 中,用于表示分配给类型为 Expression<TDelegate> 的变量的 Lambda 表达式。 

表达式目录树还可用于创建动态 LINQ 查询。有关更多信息,请参见如何:使用表达式目录树来生成动态查询。如果要生成 LINQ 提供程序,您也可以使用表达式目录树。有关更多信息,请参见演练:创建 IQueryable LINQ 提供程序


(啊哈,原来微软官方的文档是把Expression tree翻译为“表达式目录树”的啊,以前还一直不知道呢。总之我还是继续用英文标识避免歧义。) 

Expression tree、委托与lambda表达式 

委托(delegate)与lambda表达式 

了解过C# 3.0的新特性的话应该知道,在C# 3.0中新引入了一个语法结构,称为lambda expression(lambda表达式/匿名函数)。对此尚不了解的也可以到MSDN上看看,Lambda表达式。Lambda表达式既可以赋值给一个委托(delegate)类型,例如Action、Func等系列的内建委托类型;也可以赋值给Expression<TDelegate>类型,例如以下lambda表达式: 

C#代码  收藏代码
  1. x => -x  


当它被直接赋值给Func<int, int>类型的变量时,C#编译器会将它的内容编译为一个静态方法,并创建一个对应类型的引用赋值给变量。也就是说,对于 

C#代码  收藏代码
  1. static class Program {  
  2.     static void Main( string[ ] args ) {  
  3.         Func<intint> negateFunc = x => -x;  
  4.     }  
  5. }  


C#编译器会编译为类似下面的代码: 

C#代码  收藏代码
  1. internal static class Program  
  2.     [CompilerGenerated]  
  3.     private static int <Main>b__0( int x ) {  
  4.         return -x;  
  5.     }  
  6.   
  7.     private static void Main( string[ ] args ) {  
  8.         Func<intint> negateFunc = new Func<intint>( <Main>b__0 );  
  9.     }  
  10. }  


(实际上还涉及到缓存那个委托,这里省略掉了。另外,之所以会编译为一个静态方法是因为这个lambda表达式没有使用任何“自由变量”,也就是既不是参数或局部变量也不是类的成员的变量。在现有的C#编译器实现中,如果一个匿名函数使用了“this”,那么对应生成的方法会是成员方法;如果使用了其它自由变量的话则会生成一个私有内部类来存放匿名函数所使用到的自由变量,并在这个内部类里生成匿名函数对应的方法。这里作为例子选择了最简单的情况来介绍。) 

如此将一个lambda表达式编译为一个实际的函数后,其中的MSIL字节码可以为CLR所理解并执行。这样就足够实现in-memory query了,例如LINQ-to-Objects、LINQ-to-DataSet等。但其它平台无法理解MSIL,要对函数进行分析然后执行就会十分困难。例如说,如果想让一个lambda表达式在SQL Server上执行,该如何让SQL Server也理解它呢? 

Expression tree与lambda表达式 

MSIL之所以不便于分析是因为它将原本是树状结构的程序代码转换为了线性结构,损失了一些信息,主要是损失了程序代码的“结构性”,更接近于底层而降低了抽象程度。 
我们知道,程序源代码对应着具体语法树(concrete syntax tree),每个叶节点对应着代码里的一个词素,其上则是各种语法结构,如表达式、语句、声明、定义等。抽象语法树(abstract syntax tree,AST)则在具体语法树的基础上将一些诸如关键字、括号等冗余信息去掉,让树更加整洁,便于分析而不损失任何有用的信息。 

前面那个简单的lambda表达式,其对应的具体语法树如下图: 
 
Expression到Unary Expression中间的虚线表示中间省略了许多层。从Expression到最后的id,中间包括expression -> non-assignment-expression -> conditional-expression -> null-coalescing-expression -> conditional-or-expression -> conditional-and-expression -> inclusive-or-expression -> exclusive-or-expression -> and-expression -> equality-expression -> relational-expression -> additive-expression -> multiplicative-expression -> unary-expression -> - unary-expression -> primary-expression -> primary-no-array-creation-expression -> simple-name -> identifier。要是画到图上太乱了……) 
由于具体语法树忠实的对应着原本的语法定义中的各种语法结构,这棵具体语法树中不但含有用于标识lambda表达式的箭头(“=>”),用于标识取反的一元表达式的负号(“-”),还有一堆中间为了表示表达式优先级而设置的层次(也就是省略掉的部分)。这样的语法树上有着太多的冗余信息,原本很简单的代码却变成了复杂的语法树,反而不便于分析和使用了。 

既然知道这个表达式一定是一个lambda表达式,那个箭头就可以去掉;得知方法体是一个取反的一元表达式之后,负号也可以去掉;用于表示中间层次的表达式层次也全部可以去掉。将冗余信息抽取掉之后,可以得到一个等价的抽象语法树(AST): 
 
(这幅图的解释在这里有更新) 
很好,这样简单多了,而且实际有用的语法结构都保留了下来。这个AST实际上就与LINQ Expression tree的结构对应上了: 

Java代码  收藏代码
  1. LambdaExpression  
  2.     UnaryExpression (body; negation)  
  3.         ParameterExpression ("x")  
  4.     ParameterExpression (parameter; "x")  



换言之,回到代码上,如果像下面这段代码把lambda表达式“x => -x”赋值给一个Expression<Func<int,int>>类型的变量: 

C#代码  收藏代码
  1. using System;  
  2. using System.Linq.Expressions;  
  3.   
  4. static class Program {  
  5.     static void Main( string[ ] args ) {  
  6.         Expression<Func<intint>> negateExpr = x => -x;  
  7.   
  8.         Func<intint> negateFunc = negateExpr.Compile( );  
  9.   
  10.         Console.WriteLine( negateFunc( 1 ) );  
  11.     }  
  12. }  


编译器会检测到赋值目标是Expression<TDelegate>,于是并不直接生成其对应的静态方法的MSIL,而是生成创建Expression tree的代码。在C#编译器的帮助下,上面的代码完全等价于以下版本: 

C#代码  收藏代码
  1. using System;  
  2. using System.Linq.Expressions;  
  3.   
  4. static class Program {  
  5.     static void Main( string[ ] args ) {  
  6.         ParameterExpression param = Expression.Parameter(typeof(int), "x");  
  7.         Expression<Func<intint>> negateExpr =  
  8.             Expression.Lambda<Func<intint>>(  
  9.                 Expression.Negate( param ),  
  10.                 new ParameterExpression[ ] { param } );  
  11.   
  12.         Func<intint> negateFunc = negateExpr.Compile( );  
  13.   
  14.         Console.WriteLine( negateFunc( 1 ) );  
  15.     }  
  16. }  


留意到第二个版本中是如何手工创建Expression tree的,只有这一步不同而已;将expression tree编译为委托类型,然后调用委托的部分是完全一样的。 

LINQ的Expression<TDelegate>与其对应的TDelegate委托类型之间的关系是: 
1、同一个lambda表达式既可以赋值给Expression<TDelegate>类型的变量也可以赋值给TDelegate类型的委托变量; 
2、一个Expression<TDelegate>可以通过调用其Compile<TDelegate>()转换为一个TDelegate类型的委托(反之则不行)。 

LINQ的IQueryable<T>与Expression tree 

上面介绍了Expression tree与委托的关系,但是乍一看这跟LINQ的联系却不明显。 
需要知道,在C# 3/VB 9里的查询表达式实际上会被翻译为针对IEnumerable<T>或者IQueryable<T>的扩展方法;这些扩展方法接收的参数就包括了Func<>系列的委托和Expression<TDelegate>。 

举个简单的例子,这个C# 3的查询表达式: 

C#代码  收藏代码
  1. from s in new [ ] { "a""b""cd""efg" }  
  2. where s.Length == 1  
  3. orderby s descending  
  4. select "--" + s + "--"  


会被翻译为: 

C#代码  收藏代码
  1. new [ ] { "a""b""cd""efg" } )  
  2.     .Where( s => s.Length == 1 )  
  3.     .OrderByDescending( s => s )  
  4.     .Select( s => "--" + s + "--" )  


.NET Framework中的数组由Array类实现,而它实现了IEnumerable<T>接口,所以可以直接通过Enumerable提供的扩展方法用于LINQ查询。上面的例子就是一个典型的LINQ-to-Objects的应用。 
针对IEnumerable<T>的LINQ扩展方法大都接受Func<>系的委托作为参数,例如上面的Where<T>( this IEnumerable<T>, Func<T, bool> ),OrderByDescending<TSource,TKey>( this IEnumerable<TSource>, Func<TSource,TKey> ),Select<TSource,TResult>( this IEnumerable<TSource>, Func<TSource,TResult> )等。于是,作为实际参数传进去的lambda表达式就被编译为了委托。 
但如果把例子稍微改动一下,使用IQueryable<T>系的方法的话:(通过AsQueryable()方法可以将一个IEnumerable<T>转换为IQueryable<T>来使用) 

C#代码  收藏代码
  1. from s in ( new [ ] { "a""b""cd""efg" } ).AsQueryable( )  
  2. where s.Length == 1  
  3. orderby s descending  
  4. select "--" + s + "--"  


会被翻译为: 

C#代码  收藏代码
  1. new [ ] { "a""b""cd""efg" } ).AsQueryable( )  
  2.     .Where( s => s.Length == 1 )  
  3.     .OrderByDescending( s => s )  
  4.     .Select( s => "--" + s + "--" )  


这里针对IQueryable<T>(IEnumerable<T>的一个子接口)的扩展方法却都变成接收Expression<Func<>>系的Expression tree作为参数了;对应上面的几个方法的signature,只要在Func<>外面包装一层Expression<>就是IQueryable<T>版LINQ查询用的扩展方法的signature。至于为什么要用Expression tree,前面提到过,MSIL不便于在.NET以外的平台上分析和执行,所以委托不合适;Expression tree保留了更多的结构信息,抽象层次比MSIL高,相对来说更适合这项工作。在一个LINQ-to-SQL的查询中,其IQueryProvider会负责将Expression tree优化并转换到SQL,再交给后台的数据库来执行,等获得了执行结果后将执行结果返回给调用者。有了这个途径,LINQ就能与许多平台连接起来,为.NET上的程序对多种平台的数据访问提供统一的访问方式/API。 

创建Expression tree的节点 

简单介绍过LINQ中的Expression tree的概念后,让我们来讨论一下这棵树里面的节点该如何创建。LINQ Expression tree中的节点一般是通过System.Linq.Expression.Expression这个类上的工厂方法来创建的。包括以下的一些方法: 
(方法名:返回值) 

Java代码  收藏代码
  1. Add                : BinaryExpression  
  2. AddChecked         : BinaryExpression  
  3. And                : BinaryExpression  
  4. AndAlso            : BinaryExpression  
  5. ArrayIndex         : BinaryExpression  
  6. ArrayLength        : UnaryExpression  
  7. Call               : MethodCallExpression  
  8. Coalesce           : BinaryExpression  
  9. Condition          : ConditionalExpression  
  10. Constant           : ConstantExpression  
  11. Convert            : UnaryExpression  
  12. ConvertChecked     : UnaryExpression  
  13. Divide             : BinaryExpression  
  14. Equal              : BinaryExpression  
  15. ExclusiveOr        : BinaryExpression  
  16. Field              : MemberExpression  
  17. GreaterThan        : BinaryExpression  
  18. GreaterThanOrEqual : BinaryExpression  
  19. Invoke             : InvocationExpression  
  20. Lambda             : Expression<TDelegate>  
  21. LeftShift          : BinaryExpression  
  22. LessThan           : BinaryExpression  
  23. LessThanOrEqual    : BinaryExpression  
  24. ListInit           : ListInitExpression  
  25. MakeBinary         : BinaryExpression  
  26. MakeMemberAccess   : MemberExpression  
  27. MakeUnary          : UnaryExpression  
  28. MemberInit         : MemberInitExpression  
  29. Modulo             : BinaryExpression  
  30. Multiply           : BinaryExpression  
  31. MultiplyChecked    : BinaryExpression  
  32. Negate             : UnaryExpression  
  33. NegateChecked      : UnaryExpression  
  34. New                : NewExpression  
  35. NewArrayBounds     : NewArrayExpression  
  36. NewArrayInit       : NewArrayExpression  
  37. Not                : UnaryExpression  
  38. NotEqual           : BinaryExpression  
  39. Or                 : BinaryExpression  
  40. OrElse             : BinaryExpression  
  41. Parameter          : ParameterExpression  
  42. Power              : BinaryExpression  
  43. Property           : MemberExpression  
  44. PropertyOrField    : MemberExpression  
  45. Quote              : UnaryExpression  
  46. RightShift         : BinaryExpression  
  47. Subtract           : BinaryExpression  
  48. SubtractChecked    : BinaryExpression  
  49. TypeAs             : UnaryExpression  
  50. TypeIs             : TypeBinaryExpression  
  51. UnaryPlus          : UnaryExpression  


上述列表中每个名字其实对应着一组重载,参数不同,但返回值的类型是一样的。这些工厂方法所生成的表达式的意义应该说还是很直观的,就不特别介绍了。经常跟编译器打交道的话应该对这些名字尤其熟悉。 

这些工厂方法对应的类的层次结构是: 

Java代码  收藏代码
  1. System.Object  
  2.   System.Linq.Expressions.Expression  
  3.     System.Linq.Expressions.BinaryExpression  
  4.     System.Linq.Expressions.ConditionalExpression  
  5.     System.Linq.Expressions.ConstantExpression  
  6.     System.Linq.Expressions.InvocationExpression  
  7.     System.Linq.Expressions.LambdaExpression  
  8.       System.Linq.Expressions.Expression<TDelegate>  
  9.     System.Linq.Expressions.MemberExpression  
  10.     System.Linq.Expressions.MethodCallExpression  
  11.     System.Linq.Expressions.NewExpression  
  12.     System.Linq.Expressions.NewArrayExpression  
  13.     System.Linq.Expressions.MemberInitExpression  
  14.     System.Linq.Expressions.ListInitExpression  
  15.     System.Linq.Expressions.ParameterExpression  
  16.     System.Linq.Expressions.TypeBinaryExpression  
  17.     System.Linq.Expressions.UnaryExpression  


观察可得知,LINQ Expression tree中的类只能用于表现表达式,而无法表现“更大”的语法结构,例如赋值表达式、一般语句等。这是LINQ v1的一个重要限制,也是为什么C# 3.0的规范里明确提到一个能赋值给Expressin<TDelegate>类型的lambda表达式的方法体只能是一个表达式,而不能是一个代码块——代码块无法通过LINQ v1里的Expression tree表示。同时也值得注意的是,LINQ Expression tree本质上是一棵静态类型的树——所有节点所表示的类型都能够在编译时确定。这一点在后面提到DLR时会再展开讨论。 

修改Expression tree 

一棵Expession tree在创建后就不可再改变。假如某个程序接收一棵Expression tree为参数,然后仅仅是用于生成别的形式的代码(例如SQL语句),那么这个不可改变性不会有什么影响。但如果一个程序想对一棵Expression tree进行修改该怎么办呢?解决方法是从来源的Expression tree复制出一棵新的树,在复制过程中根据自己的需要选择是直接复制原有节点还是创建修改了的节点。MSDN上有一个例子解释了如何实现这种需求,如何:修改表达式目录树。 

更详细的LINQ Expression tree的讨论留待以后再说。下一篇将简单介绍DLR的状况,以及它与LINQ Expression tree的关系。 

P.S. 这篇文发了之后才发现上个月博客园的TerryLee也写了类似的介绍,在这里:打造自己的LINQ Provider(上):Expression Tree揭秘,值得一读。 

=========================================================================== 

文中的两幅树状图是用Graphviz的dot来绘制的,代码如下: 
cst.dot: 

Java代码  收藏代码
  1. digraph ExpressionTree {  
  2.   
  3. node [fontsize=12, fontcolor=blue, font=Courier, shape=box]  
  4.   
  5.  // node declarations  
  6.  lambda [label="Lambda Expression"]  
  7.    
  8.  anoFuncSig [label="Anonymous Function\nSignature"]  
  9.  arrow [label="=>"]  
  10.  anoFuncBody [label="Anonymous Function\nBody"]  
  11.    
  12.  impFuncSig [label="Implicit Anonymous\nFunction Signature"]  
  13.  expr [label="Expression"]  
  14.    
  15.  impParam [label="Implicit Anonymous\nFunction Parameter"]  
  16.  uexpr1 [label="Unary Expression"]  
  17.    
  18.  neg [label="-"]  
  19.  uexpr2 [label="Unary Expression"]  
  20.    
  21.  id [label="Identifier:\nx"]  
  22.    
  23.  simpName [label="Simple Name:\nx"]  
  24.    
  25.  // relations  
  26.   
  27.  lambda -> anoFuncSig  
  28.  lambda -> arrow  
  29.  lambda -> anoFuncBody  
  30.    
  31.  {rank=same; anoFuncSig arrow anoFuncBody }  
  32.    
  33.  anoFuncSig -> impFuncSig  
  34.  anoFuncBody -> expr  
  35.    
  36.  {rank=same; impFuncSig expr }  
  37.    
  38.  impFuncSig -> impParam  
  39.  expr -> uexpr1 [style=dashed]  
  40.   
  41.  {rank=same; impParam uexpr1 }  
  42.   
  43.  impParam -> id  
  44.  uexpr1 -> neg  
  45.  uexpr1 -> uexpr2  
  46.    
  47.  {rank=same; id neg uexpr2 }  
  48.    
  49.  uexpr2 -> simpName  
  50. }  


ast.dot: 

Java代码  收藏代码
    1. digraph ExpressionTree {  
    2.   
    3. node [fontsize=12, fontcolor=blue, font=Courier, shape=box]  
    4. edge [fontsize=10, fontcolor=purple]  
    5.  // node declarations  
    6.  lambda [label="Lambda Expression"]  
    7.    
    8.  param [label="Parameter:\nx"]  
    9.  body [label="Unary Expression\n(Negation)"]  
    10.    
    11.  param2 [label="Simple Name:\nx"]  
    12.    
    13.  // relations  
    14.   
    15.  lambda -> param [label="Signature"]  
    16.  lambda -> body [label="Body"]  
    17.    
    18.  {rank=same; param body }  
    19.    
    20.  body -> param2  
    21.  param -> param2 [label="(same node)", fontsize=8, style=dashed, dir=both]  
    22. }  
posted @ 2014-03-31 16:08  脾气不坏  阅读(401)  评论(0)    收藏  举报