痛而后能善
无惧于闯
Make a greate impact

Linq 之Expression Tree再思考
一、         Expression Tree 定义

根据MSDNExpression Tree的描述表达式目录树是一种数据结构,树中的每个节点都表示一个表达式,C#的语法定义为

public sealed class Expression<TDelegate> :LambdaExpression

由定义知,它主要继承于LambdaExpression。这里就需要和C#3.0的特性Lambdas表达式联系起来了,“Lambda 表达式是一个匿名函数,它可以包含表达式和语句,并且可用于创建委托或表达式目录树类型。例如:

Func<double,string> TestLambda = (mark)=>mark>= 60 ? "Pass" : "Fail";
Expression
<Func<double,string>> Test = (mark)=>mark>= 60 ? "Pass" : "Fail";

既然Lambda表达式已经实现了相应的功能,表达式目录树的作用在哪呢?这需要和函数式编程联系起来,如果用过LISP的朋友也许比较清楚。还有一方面可能在编译器前端上,词法分析器和语法分析器在组装表达式的结果常常是语法树,后端则可以进一步处理。

二、         Expression Tree执行、访问、修改、应用

(一)   Expression Tree执行
在执行表达式目录树之前,需要了解Expression命名空间的相关信息。该空间包含了构建Expression的类型,接口和枚举。抽象类Expression则提供用于表达式目录树架构的根节点。Expression命名空间主要包含了以下类型: 
 

老赵在《扩展LINQ to SQL:使用Lambda Expression批量删除数据》提到的BinaryExpressionExpression Tree的一个关键,先来看看BinaryExpression的定义,它表示包含二元运算符的表达式。下表包括了特定的运算类型和同名的工厂方法。 

二元算术运算

按位运算

移位运算

条件布尔运算

比较运算

 

 

Add

And

LeftShift

AndAlso

Equal

 

 

AddChecked

Or

RightShift

OrElse

NotEqual

Divide

ExclusiveOr

GreaterThanOrEqual

Modulo

GreaterThan

Multiply

LessThan

MultiplyChecked

LessThanOrEqual

Power

合并运算

 

数组索引运算

Subtract

Coalesce

 

ArrayIndex

SubtractChecked

而枚举则为: ExpressionType(描述表达式目录树的节点的节点类型)MemberBindingType(描述 MemberInitExpression对象中使用的绑定类型)。那么表达式目录树是如何执行的呢?

MSDN《如何:执行表达式目录树》,可以看到表达式目录树主要是由各种类型的表达式按照预先设定的函数形式有步骤地实现。虽然文中曾经提到:如果委托的类型未知,也就是说,lambda 表达式属于 LambdaExpression类型,而不是属于 Expression<(Of <(TDelegate>)>) 类型,则必须对该委托调用 DynamicInvoke 方法,而不是直接调用该委托。然而接下的代码中并没有给出LambdaExpression类型的代码。陈黎夫翻译的Aaron Erickson谈论LINQi4o就给出了一个相当明确的例子,下面的代码以加法运算来做一个简单的对比(没有检查溢出问题,可以用AddChecked)

//以下表达式目录树实现:(int x, int y)=>x+y
ParameterExpression xParam=Expression.Parameter(typeof(int),"x");
ParameterExpression yParam
=Expression.Parameter(typeof(int),"y");
ParameterExpression[] allParam
={xParam,yParam};
BinaryExpression be 
= Expression.Add(xParam,yParam);
Expression
<Func<intintint>> le = 
           Expression.Lambda
<Func<intint,int>>(be,allParam);
Func
<intintint> addExpression = le.Compile();
Console.WriteLine(addExpression(
11));
//LambdaExpression类型的代码
LambdaExpression le2 = 
          Expression.Lambda
<Func<intintint>>(be, allParam);
Console.WriteLine(le2.Compile().DynamicInvoke(
12));
回顾一下,如果要写一个对分数进行判断及格与否的函数Test,可以这样写
public string Testr(double mark)
{
    
return mark >= 60?"Pass":"Fail";
}

如果用表达式目录树来实现的话,则
//实现的函数:(mark)=>mark>= 60 ? "Pass" : "Fail";
ConstantExpression passPE = Expression.Constant("Pass",typeof(string));
ConstantExpression failPE 
= Expression.Constant("Fail"typeof(string));
ParameterExpression left2
=Expression.Parameter(typeof(double),"mark");
ConstantExpression right2 
= Expression.Constant(60D, typeof(double));
BinaryExpression ce 
= Expression.GreaterThanOrEqual(left2, right2);
ConditionalExpression be2 
= Expression.Condition(ce, passPE, failPE);
Expression
<Func<doublestring>> EnglishTest = 
       Expression.Lambda
<Func<doublestring>>(be2, left2);
Console.WriteLine(EnglishTest.Compile().DynamicInvoke(
50));
// 另外一种方式
ParameterExpression markParam = Expression.Parameter(typeof(double),"mark");
     LambdaExpression PETest 
=
           Expression.Lambda
<Func<doublestring>>(
               Expression.Condition(
                  Expression.GreaterThanOrEqual(
                         markParam,Expression.Constant(60D,
typeof(double))
                      ),
                    Expression.Constant(
"Pass",typeof(string)),
                        Expression.Constant(
"Fail",typeof(string))
                    ), markParam
                );
Console.WriteLine(PETest.Compile().DynamicInvoke(
90));
// Lambda表达式写法
Expression<Func<double,string>> Test = (mark)=>mark>= 60 ? "Pass" : "Fail";
Console.WriteLine(Test.Compile().DynamicInvoke(
90));
在写表达式目录树的时候,需要注意一点,就是表达式目录树无法由语句体转换而来。如,
Expression<Func<int, int, bool>> equal = (left, right) => left == right;一般情况下,Expression TreeBody部分由lambdas表达式来实现,这一点我们从它的定义中可以得出原因,但是以下的定义却会出现问题:
Expression<Func<int, int, bool>> equal = (left, right) => { return left == right; };
编译器显示的错误为“无法将具有语句体的 lambda 表达式转换为表达式目录树”。

(二) 访问表达式目录树
MSDNSample中有一个非常好的代码Expression Tree Visualizer,园子里的works guo Expression Tree Visualizer的使用 中非常详细的讲解了这个工具的用法,大家可以参考一下,Msdn中也有一篇非常详细的文章介绍《如何访问表达式目录树》这里必须要提到的是ExpressionType枚举,根据msdn的定义,它是描述表达式目录树的节点的节点类型,而且也是访问Expression的基础。从《如何:实现表达式目录树访问器》一文中,详细的讲述了访问的具体方法,而且代码重用非常方便。以下对该代码做一个简单的图解。从代码的访问方向来看,主要包括了三大类如图一所示,图二则对每一大类所包含的节点类型列举出来了(由于水平有限所以把others包含了剩下的所有类型)

                      图一

                            图二
MSDN中的代码主要实现方式是什么呢?从根部直到树叶逐一分离,由于每一级的表达式有可能包含下一级的表达式子树,所以它有可能是通过类似递归的方式来实现的。比如,其中的条件表达式。

protected virtual Expression Visit(Expression exp)
{
    ……
       
switch (exp.NodeType)
        
{
        ……
        
case ExpressionType.Conditional:
                
return this.VisitConditional((ConditionalExpression)exp);
        ……
    }

    ……
}

protected virtual Expression VisitConditional(ConditionalExpression c)
{
        Expression test 
= this.Visit(c.Test);
        Expression ifTrue 
= this.Visit(c.IfTrue);
        Expression ifFalse 
= this.Visit(c.IfFalse);
        
if (test != c.Test || ifTrue != c.IfTrue || ifFalse != c.IfFalse)
        
{
            
return Expression.Condition(test, ifTrue, ifFalse);
        }

        
return c;
}

再次以Expression<Func<double,string>> Test = (mark)=>mark>= 60 ? "Pass" : "Fail";为例。
首先,访问器遇到的NodeType是lambda,而lambda表达式中包含了一个参数表达式
Expression.Parameter(typeof(double),"mark");和一个条件表达式Expression.Condition(),这一条件表达式下包括了一个二元表达式Expression.GreaterThanOrEqual(markParam,Expression.Constant(60D,typeof(double))), 和两个常量表达式                        Expression.Constant("Pass",typeof(string)),Expression.Constant("Fail",typeof(string)),最后二元表达式的左边和右边为对参数表达式的引用和一个常量表达式(如图三所示)。

 

Msdn中的表达式目录树曾经提到“本主题演示如何修改表达式目录树。表达式目录树是不可变的,这意味着不能直接修改表达式目录树。若要更改表达式目录树,必须创建现有表达式目录树的一个副本,并在创建副本的过程中执行所需更改。您可以使用表达式目录树访问器遍历现有表达式目录树,并复制它访问的每个节点。”原因在那里呢?由于Expression Tree是由不同的Expression组成的,不同的Expression中包含着不同的属性,比如ConditionalExpression中的Test属性获取条件运算的测试,它的语法为: public Expression Test { get; }只能够获取不能够设置,所以不能够直接修改,而必须像它所说的那样遍历表达式树找到修改的地方,再置换表达式。前面提到的代码:

ConstantExpression passPE = Expression.Constant("Pass",typeof(string));
ConstantExpression failPE 
= Expression.Constant("Fail"typeof(string));
ParameterExpression left 
=Expression.Parameter(typeof(double),"mark");
ConstantExpression right 
= Expression.Constant(60D, typeof(double));
BinaryExpression be 
= Expression.GreaterThanOrEqual(left, right);
ConditionalExpression ce 
= Expression.Condition(be, passPE, failPE);
Expression
<Func<doublestring>> EnglishTest = 
            Expression.Lambda
<Func<doublestring>>(ce, left2);
 如果要修改及格的分数为50分,那么必须替换相应的常量表达式,然后再次实现表达式目录树,即
ConstantExpression rightNew = Expression.Constant(50D, typeof(double));
BinaryExpression beNew 
= Expression.GreaterThanOrEqual(left, rightNew);
ConditionalExpression ceNew 
= Expression.Condition(beNew, passPE, failPE);
Expression
<Func<doublestring>>  TestNew = 
          Expression.Lambda
<Func<doublestring>>(ceNew, left);

MSDN如何:修改表达式目录树则是通过重载VisitBinary方法完成的。
 
(三)   Expression Tree 作用
首先,为什么要定义这样一种数据结构?目的很简单,就是为了在执行代码和数据之间的转换。这当中又有什么缘由呢?其
实,一个表达式就是一种抽象语法树。而抽象语法树是一种表示已经被解释的源代码的数据结构。返回看之前的代码,是不是清楚一些呢?这种执行代码和数据之间转换的好处在哪里呢?我们知道在
.net下所有的代码最终会被编译成IL代码。假设需要执行LINQ To SQL的代码,比如

var query = from employees in nDC.Employees
            
where employees.Country.ToUpper() == "USA"
            select employees;
则必须要把查询表达式转换为相应的SQL语句
{SELECT [t0].[EmployeeID], [t0].[LastName], [t0].[FirstName], 
           [t0].[Title], [t0].[TitleOfCourtesy], [t0].[BirthDate], 
           [t0].[HireDate], [t0].[Address], [t0].[City], [t0].[Region], 
           [t0].[PostalCode], [t0].[Country], [t0].[HomePhone], 
           [t0].[Extension], [t0].[Photo], [t0].[Notes], [t0].[ReportsTo], 
           [t0].[PhotoPath] FROM [dbo].[Employees] AS [t0] 
          WHERE UPPER([t0].[Country]) 
= @p0}

然后在Server上执行,那么生成的树状结构有利于代码的运行。

其次,表达式目录树可用于做动态查询。MSDN《如何:使用表达式目录树来生成动态查询》提到的“应用程序可能会提供一个用户界面,最终用户可以使用该用户界面指定一个或多个谓词来筛选数据。为了使用 LINQ 进行查询,这种应用程序必须使用表达式目录树在运行时创建 LINQ 查询。”Linq To Object, 尝试了一下虽然不是很方便但很实用。
double[] marks = 90,70,45,33,88,67};
IQueryable
<double>marksData=marks.AsQueryable<double>();
BinaryExpression bodyExpression
=
             Expression.GreaterThanOrEqual(markParam,right2);
MethodCallExpression whereCallExpression
=Expression.Call(
    
typeof(Queryable),
    
"Where",new Type[]{marksData.ElementType},marksData.Expression,
  Expression.Lambda
<Func<double,bool>>
     
(bodyExpression,new ParameterExpression[]{markParam})
                );
IQueryable
<double> results = 
         marksData.Provider.CreateQuery
<double>(whereCallExpression);

  李永京则在《LINQ体验(17)——LINQ to SQL语句之动态查询》一文中阐述了更为详细的用法
IQueryable<Customer> custs = db.Customers;
//创建一个参数c
ParameterExpression param = 
    Expression.Parameter(
typeof(Customer), "c");
//c.City=="London"
Expression left = Expression.Property(param,
    
typeof(Customer).GetProperty("City"));
Expression right 
= Expression.Constant("London");
Expression filter 
= Expression.Equal(left, right);

Expression pred 
= Expression.Lambda(filter, param);
//Where(c=>c.City=="London")
Expression expr = Expression.Call(typeof(Queryable), "Where",
    
new Type[] typeof(Customer) }
    Expression.Constant(custs), pred);
//生成动态查询
IQueryable<Customer> query = db.Customers.AsQueryable()
    .Provider.CreateQuery
<Customer>(expr);
 代码方面推荐的是Scott Guthrie的博客堂中《动态LINQ (第一部分:使用LINQ动态查询库)》中提供了最为简便的方式,通过把字符串连接起来构建成动态SQL查询的方式来处理。并提供了动态查询库。经过优化后以下的代码
NorthwindDataContext nDC=new NorthwindDataContext();
var query 
= from employees in nDC.Employees
            
where employees.Country.ToUpper() == "USA"
            select employees;
 就可以改为:var query = nDC.Employees.Where("Country.ToUpper() == @0", "USA").Select("New(LastName as Name)");
  相当简洁。最后看一段微软是如何实现动态查询代码,
public static IQueryable Where(this IQueryable source, string predicate, 
params object[] values) {
   
if (source == nullthrow new ArgumentNullException("source");
   
if (predicate == nullthrow new ArgumentNullException("predicate");
   LambdaExpression lambda 
= DynamicExpression.ParseLambda
             (source.ElementType, 
typeof(bool), predicate, values);
   
return source.Provider.CreateQuery(
        Expression.Call(
           
typeof(Queryable), "Where",
           
new Type[] { source.ElementType },
           source.Expression, Expression.Quote(lambda)));
}
 由此就可知,表达式目录树的用途之广泛。

三、         总结

LINQ当中有诸多重要的组成部分,如Linq To ObjectLinq To SQLLinq To XML等等,其中一个非常基础而且实用的就是Expression Tree部分。从其命名空间到操作方式都为Linq的应用提供了有力的支持,也许在往后的更新中Expression Tree会为编程带来更为强大的优势。

posted on 2008-07-18 20:45  greater  阅读(2343)  评论(4编辑  收藏  举报