由浅入深表达式树(二)遍历表达式树

发布时间:2026/7/3 21:24:04
由浅入深表达式树(二)遍历表达式树 为什么要学习表达式树表达式树是将我们原来可以直接由代码编写的逻辑以表达式的方式存储在树状的结构里从而可以在运行时去解析这个树然后执行实现动态的编辑和执行代码。LINQ to SQL就是通过把表达式树翻译成SQL来实现的所以了解表达树有助于我们更好的理解 LINQ to SQL同时如果你有兴趣可以用它创造出很多有意思的东西来。表达式树是随着.NET 3.5推出的所以现在也不算什么新技术了。但是不知道多少人是对它理解的很透彻 在上一篇Lambda表达式的回复中就看的出大家对Lambda表达式和表达式树还是比较感兴趣的那我们就来好好的看一看这个造就了LINQ to SQL以及让LINQ to Everything的好东西吧。本系列计划三篇第一篇主要介绍表达式树的创建方式。第二篇主要介绍表达式树的遍历问题。第三篇将利用表达式树打造一个自己的LinqProvider。由浅入深表达式树 一创建表达式树由浅入深表达式树二遍历表达式树由浅入深表达式树三Linq to 博客园本文主要内容有返回值的表达式树示例通过表达式树访问类翻译SQL查询Where语句上一篇由浅入深表达式树一我们主要讨论了如何根据Lambda表达式以及通过代码的方式直接创建表达式树。表达式树主要是由不同类型的表达式构成的而在上文中我们也列出了比较常用的几种表达式类型由于它本身结构的特点所以用代码写起来然免有一点繁琐当然我们也不一定要从头到尾完全自己去写只有我们理解它了我们才能更好的去使用它。在上一篇中我们用代码的方式创建了一个没有返回值用到了循环以及条件判断的表达式为了加深大家对表达式树的理解我们先回顾一下看一个有返回值的例子。有返回值的表达式树123456789101112131415161718192021222324252627282930313233343536// 直接返回常量值ConstantExpression ce1 Expression.Constant(10);// 直接用我们上面创建的常量表达式来创建表达式树ExpressionFuncint expr1 Expression.LambdaFuncint(ce1);Console.WriteLine(expr1.Compile().Invoke());// 10// --------------在方法体内创建变量经过操作之后再返回------------------// 1.创建方法体表达式 2.在方法体内声明变量并附值 3. 返回该变量ParameterExpression param2 Expression.Parameter(typeof(int));BlockExpression block2 Expression.Block(new[]{param2},Expression.AddAssign(param2,Expression.Constant(20)),param2);ExpressionFuncint expr2 Expression.LambdaFuncint(block2);Console.WriteLine(expr2.Compile().Invoke());// 20// -------------利用GotoExpression返回值-----------------------------------LabelTarget returnTarget Expression.Label(typeof(Int32));LabelExpression returnLabel Expression.Label(returnTarget,Expression.Constant(10,typeof(Int32)));// 为输入参加10之后返回ParameterExpression inParam3Expression.Parameter(typeof(int));BlockExpression block3 Expression.Block(Expression.AddAssign(inParam3,Expression.Constant(10)),Expression.Return(returnTarget,inParam3),returnLabel);ExpressionFuncint,int expr3 Expression.LambdaFuncint,int(block3,inParam3);Console.WriteLine(expr3.Compile().Invoke(20));// 30我们上面列出了3个例子都可以实现在表达式树中返回值第一种和第二种其实是一样的那就是将我们要返回的值所在的表达式写在block的最后一个参数。而第三种我们是利用了goto 语句如果我们在表达式中想跳出循环或者提前退出方法它就派上用场了。这们上一篇中也有讲到Expression.Return的用法。当然我们还可以通过switch case 来返回值请看下面的switch case的用法。1234567891011121314//简单的switch case 语句ParameterExpression genderParam Expression.Parameter(typeof(int));SwitchExpression swithExpression Expression.Switch(genderParam,Expression.Constant(不详),//默认值Expression.SwitchCase(Expression.Constant(男),Expression.Constant(1)),Expression.SwitchCase(Expression.Constant(女),Expression.Constant(0))//你可以将上面的Expression.Constant替换成其它复杂的表达式,ParameterExpression, BinaryExpression等, 这也是表达式灵活的地方, 因为归根结底它们都是继承自Expression, 而基本上我们用到的地方都是以基类作为参数类型接受的,所以我们可以传递任意类型的表达式。);ExpressionFuncint,string expr4 Expression.LambdaFuncint,string(swithExpression, genderParam);Console.WriteLine(expr4.Compile().Invoke(1));//男Console.WriteLine(expr4.Compile().Invoke(0));//女Console.WriteLine(expr4.Compile().Invoke(11));//不详有人说表达式繁琐这我承认可有人说表达式不好理解恐怕我就没有办法认同了。的确表达式的类型有很多光我们上一篇列出来的就有23种但使用起来并不复杂我们只需要大概知道一些表达类型所代表的意义就行了。实际上Expression类为我们提供了一系列的工厂方法来帮助我们创建表达式就像我们上面用到的Constant, Parameter, SwitchCase等等。当然自己动手胜过他人讲解百倍我相信只要你手动的去敲一些例子你会发现创建表达式树其实并不复杂。表达式的遍历说完了表达式树的创建我们来看看如何访问表达式树。MSDN官方能找到的关于遍历表达式树的文章真的不多有一篇比较全的链接真的没有办法看下去。请问盖茨叔叔就是这样教你们写文档的么但是ExpressionVisitor是唯一一种我们可以拿来就用的帮助类所以我们硬着头皮也得把它啃下去。我们可以看一下ExpressionVisitor类的主要入口方法是Visit方法其中主要是一个针对ExpressionNodeType的switch case这个包含了85种操作类型的枚举类但是不用担心在这里我们只处理44种操作类型14种具体的表达式类型也就是说只有14个方法我们需要区别一下。我将上面链接中的代码转换成下面的表格方便大家查阅。认识了ExpressionVisitor之后下面我们就来一步一步的看看到底是如果通过它来访问我们的表达式树的。接下来我们要自己写一个类继承自这个ExpressionVisitor类然后覆盖其中的某一些方法从而达到我们自己的目地。我们要实现什么样的功能呢123456789ListUser myUsers newListUser();varuserSql myUsers.AsQueryable().Where(u u.Age 2);Console.WriteLine(userSql);// SELECT * FROM (SELECT * FROM User) AS T WHERE (Age2)ListUser myUsers2 newListUser();varuserSql2 myUsers.AsQueryable().Where(u u.NameJesse);Console.WriteLine(userSql2);// SELECT * FROM (SELECT * FROM USER) AS T WHERE (NameJesse)我们改造了IQueryable的Where方法让它根据我们输入的查询条件来构造SQL语句。要实现这个功能首先我们得知道IQueryable的Where 方法在哪里它是如何实现的12345678910111213141516171819publicstaticclassQueryable{publicstaticIQueryableTSource WhereTSource(thisIQueryableTSource source, ExpressionFuncTSource,bool predicate){if(source null){thrownewArgumentNullException(source);}if(predicate null){thrownewArgumentNullException(predicate);}returnsource.Provider.CreateQueryTSource(Expression.Call(null, ((MethodInfo)MethodBase.GetCurrentMethod()).MakeGenericMethod(newType[] {typeof(TSource) }),newExpression[] { source.Expression, Expression.Quote(predicate) }));}}通过F12我们可以跟到System.Linq下有一个Querable的静态类而我们的Where方法就是是扩展方法的形势存在于这个类中包括其的GroupByJoinLast等有兴趣的同学可以自行Reflect J。大家可以看到上面的代码中实际上是调用了Queryable的Provider的CreateQuery方法。这个Provider就是传说中的Linq Provider但是我们今天不打算细说它我们的重点在于传给这个方法的参数被转成了一个表达式树。实际上Provider也就是接收了这个表达式树然后进行遍历解释的那么我们可以不要Provider直接进行翻译吗 I SAY YES! WHY CAN’T?12345678910111213publicstaticclassQueryExtensions{publicstaticstringWhereTSource(thisIQueryableTSource source,ExpressionFuncTSource,bool predicate){varexpression Expression.Call(null, ((MethodInfo)MethodBase.GetCurrentMethod()).MakeGenericMethod(newType[] {typeof(TSource) }),newExpression[] { source.Expression, Expression.Quote(predicate) });vartranslator newQueryTranslator();returntranslator.Translate(expression);}}上面我们自己实现了一个Where的扩展方法将该Where方法转换成表达式树只不过我们没有调用Provider的方法而是直接让另一个类去将它翻译成SQL语句然后直接返回该SQL语句。接下来的问题是这个类如何去翻译这个表达式树呢我们的ExpressionVisitor要全场了123456789classQueryTranslator : ExpressionVisitor{internalstringTranslate(Expression expression){this.sb newStringBuilder();this.Visit(expression);returnthis.sb.ToString();}}首先我们有一个类继承自ExpressionVisitor里面有一个我们自己的Translate方法然后我们直接调用Visit方法即可。上面我们提到了Visit方法实际上是一个入口会根据表达式的类型调用其它的Visit方法我们要做的就是找到对应的方法重写就可以了。但是下面有一堆Visit方法我们要要覆盖哪哪些呢 这就要看我们的表达式类型了在我们的Where扩展方法中我们传入的表达式树是由Expression.Call方法构造的而它返回的是MethodCallExpression所以我们第一步是覆盖VisitMethodCall。12345678910111213protectedoverrideExpression VisitMethodCall(MethodCallExpression m){if(m.Method.DeclaringType typeof(QueryExtensions) m.Method.Name Where){sb.Append(SELECT * FROM ();this.Visit(m.Arguments[0]);sb.Append() AS T WHERE );LambdaExpression lambda (LambdaExpression)StripQuotes(m.Arguments[1]);this.Visit(lambda.Body);returnm;}thrownewNotSupportedException(string.Format(方法{0}不支持, m.Method.Name));}代码很简单方法名是Where那我们就直接开始拼SQL语句。重点是在这个方法里面两次调用了Visit方法我们要知道它们会分别调用哪两个具体的Visit方法我们要做的就是重写它们。第一个我们就不说了大家可以下载源代码自己去调试一下我们来看看第二个Visit方法。很明显我们构造了一个Lambda表达式树但是注意我们没有直接Visit这Lambda表达式树它是Visit了它的Body。它的Body是什么如果我的条件是Age7这就是一个二元运算不是么所以我们要重写VisitBinary方法Let’s get started。12345678910111213141516171819202122232425262728293031323334353637protectedoverrideExpression VisitBinary(BinaryExpression b){sb.Append(();this.Visit(b.Left);switch(b.NodeType){caseExpressionType.And:sb.Append( AND );break;caseExpressionType.Or:sb.Append( OR);break;caseExpressionType.Equal:sb.Append( );break;caseExpressionType.NotEqual:sb.Append( );break;caseExpressionType.LessThan:sb.Append( );break;caseExpressionType.LessThanOrEqual:sb.Append( );break;caseExpressionType.GreaterThan:sb.Append( );break;caseExpressionType.GreaterThanOrEqual:sb.Append( );break;default:thrownewNotSupportedException(string.Format(“二元运算符{0}不支持”, b.NodeType));}this.Visit(b.Right);sb.Append());returnb;}我们根据这个表达式的操作类型转换成对应的SQL运算符我们要做的就是把左边的属性名和右边的值加到我们的SQL语句中。所以我们要重写VisitMember和VisitConstant方法。1234567891011121314151617181920212223242526272829303132333435363738394041424344protectedoverrideExpression VisitConstant(ConstantExpression c){IQueryable q c.ValueasIQueryable;if(q !null){// 我们假设我们那个Queryable就是对应的表sb.Append(SELECT * FROM );sb.Append(q.ElementType.Name);}elseif(c.Value null){sb.Append(NULL);}else{switch(Type.GetTypeCode(c.Value.GetType())){caseTypeCode.Boolean:sb.Append(((bool)c.Value) ? 1 : 0);break;caseTypeCode.String:sb.Append();sb.Append(c.Value);sb.Append();break;caseTypeCode.Object:thrownewNotSupportedException(string.Format(The constant for {0} is not supported, c.Value));default:sb.Append(c.Value);break;}}