Linq 之Expression Tree再思考
根据MSDN对Expression Tree的描述表达式目录树是一种数据结构,树中的每个节点都表示一个表达式,C#的语法定义为
由定义知,它主要继承于LambdaExpression。这里就需要和C#3.0的特性Lambdas表达式联系起来了,“Lambda 表达式”是一个匿名函数,它可以包含表达式和语句,并且可用于创建委托或表达式目录树类型。例如:
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批量删除数据》提到的BinaryExpression是Expression 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 |
从MSDN中《如何:执行表达式目录树》,可以看到表达式目录树主要是由各种类型的表达式按照预先设定的函数形式有步骤地实现。虽然文中曾经提到:如果委托的类型未知,也就是说,lambda 表达式属于 LambdaExpression类型,而不是属于 Expression<(Of <(TDelegate>)>) 类型,则必须对该委托调用 DynamicInvoke 方法,而不是直接调用该委托。然而接下的代码中并没有给出LambdaExpression类型的代码。陈黎夫翻译的《Aaron Erickson谈论LINQ和i4o》就给出了一个相当明确的例子,下面的代码以加法运算来做一个简单的对比(没有检查溢出问题,可以用AddChecked)
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<int, int, int>> le =
Expression.Lambda<Func<int, int,int>>(be,allParam);
Func<int, int, int> addExpression = le.Compile();
Console.WriteLine(addExpression(1, 1));
//LambdaExpression类型的代码
LambdaExpression le2 =
Expression.Lambda<Func<int, int, int>>(be, allParam);
Console.WriteLine(le2.Compile().DynamicInvoke(1, 2));
{
return 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<double, string>> EnglishTest =
Expression.Lambda<Func<double, string>>(be2, left2);
Console.WriteLine(EnglishTest.Compile().DynamicInvoke(50));
// 另外一种方式
ParameterExpression markParam = Expression.Parameter(typeof(double),"mark");
LambdaExpression PETest =
Expression.Lambda<Func<double, string>>(
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 Tree的Body部分由lambdas表达式来实现,这一点我们从它的定义中可以得出原因,但是以下的定义却会出现问题:
Expression<Func<int, int, bool>> equal = (left, right) => { return left == right; };
编译器显示的错误为“无法将具有语句体的 lambda 表达式转换为表达式目录树”。
(二) 访问表达式目录树
图一
图二
MSDN中的代码主要实现方式是什么呢?从根部直到树叶逐一分离,由于每一级的表达式有可能包含下一级的表达式子树,所以它有可能是通过类似递归的方式来实现的。比如,其中的条件表达式。
{
……
switch (exp.NodeType)
{
……
case ExpressionType.Conditional:
return this.VisitConditional((ConditionalExpression)exp);
……
}
……
}
{
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;
}
首先,访问器遇到的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 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<double, string>> EnglishTest =
Expression.Lambda<Func<double, string>>(ce, left2);
BinaryExpression beNew = Expression.GreaterThanOrEqual(left, rightNew);
ConditionalExpression ceNew = Expression.Condition(beNew, passPE, failPE);
Expression<Func<double, string>> TestNew =
Expression.Lambda<Func<double, string>>(ceNew, left);
MSDN中《如何:修改表达式目录树》则是通过重载VisitBinary方法完成的。
(三) Expression Tree 作用
首先,为什么要定义这样一种数据结构?目的很简单,就是为了在执行代码和数据之间的转换。这当中又有什么缘由呢?其
实,一个表达式就是一种抽象语法树。而抽象语法树是一种表示已经被解释的源代码的数据结构。返回看之前的代码,是不是清楚一些呢?这种执行代码和数据之间转换的好处在哪里呢?我们知道在.net下所有的代码最终会被编译成IL代码。假设需要执行LINQ To SQL的代码,比如
where employees.Country.ToUpper() == "USA"
select employees;
[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, 尝试了一下虽然不是很方便但很实用。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语句之动态查询》一文中阐述了更为详细的用法:
//创建一个参数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);
var query = from employees in nDC.Employees
where employees.Country.ToUpper() == "USA"
select employees;
相当简洁。最后看一段微软是如何实现动态查询代码,
params object[] values) {
if (source == null) throw new ArgumentNullException("source");
if (predicate == null) throw 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 Object、Linq To SQL和Linq To XML等等,其中一个非常基础而且实用的就是Expression Tree部分。从其命名空间到操作方式都为Linq的应用提供了有力的支持,也许在往后的更新中Expression Tree会为编程带来更为强大的优势。