热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

为什么要学习表达式树?表达式树是将我们原来可以直接由代码编写的逻辑以表达式的方式存储在树状的结构里,从而可以在运行时去解析这个树,然后执行,实现动态的编辑和执行代码。LINQt

  为什么要学习表达式树?表达式树是将我们原来可以直接由代码编写的逻辑以表达式的方式存储在树状的结构里,从而可以在运行时去解析这个树,然后执行,实现动态的编辑和执行代码。LINQ to SQL就是通过把表达式树翻译成SQL来实现的,所以了解表达树有助于我们更好的理解 LINQ to SQL,同时如果你有兴趣,可以用它创造出很多有意思的东西来。

  表达式树是随着.NET 3.5推出的,所以现在也不算什么新技术了。但是不知道多少人是对它理解的很透彻, 在上一篇Lambda表达式的回复中就看的出大家对Lambda表达式和表达式树还是比较感兴趣的,那我们就来好好的看一看这个造就了LINQ to SQL以及让LINQ to Everything的好东西吧。

  本系列计划三篇,第一篇主要介绍表达式树的创建方式。第二篇主要介绍表达式树的遍历问题。第三篇,将利用表达式树打造一个自己的LinqProvider。 



  • 由浅入深表达式树 (一)创建表达式树

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

  • 由浅入深表达式树(三)Linq to 博客园

  本文主要内容:



  • 有返回值的表达式树示例

  • 通过表达式树访问类翻译SQL查询Where语句

  上一篇由浅入深表达式树(一)我们主要讨论了如何根据Lambda表达式以及通过代码的方式直接创建表达式树。表达式树主要是由不同类型的表达式构成的,而在上文中我们也列出了比较常用的几种表达式类型,由于它本身结构的特点所以用代码写起来然免有一点繁琐,当然我们也不一定要从头到尾完全自己去写,只有我们理解它了,我们才能更好的去使用它。

  在上一篇中,我们用代码的方式创建了一个没有返回值,用到了循环以及条件判断的表达式,为了加深大家对表达式树的理解,我们先回顾一下,看一个有返回值的例子。


有返回值的表达式树

// 直接返回常量值
ConstantExpression ce1 = Expression.Constant(10);

// 直接用我们上面创建的常量表达式来创建表达式树
Expression> expr1 = Expression.Lambda>(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
);
Expression> expr2 = Expression.Lambda>(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 inParam3=Expression.Parameter(typeof(int));
BlockExpression block3 = Expression.Block(
Expression.AddAssign(inParam3,Expression.Constant(10)),
Expression.Return(returnTarget,inParam3),
returnLabel);
Expression> expr3 = Expression.Lambda>(block3,inParam3);
Console.WriteLine(expr3.Compile().Invoke(20));
// 30

    我们上面列出了3个例子,都可以实现在表达式树中返回值,第一种和第二种其实是一样的,那就是将我们要返回的值所在的表达式写在block的最后一个参数。而第三种我们是利用了goto 语句,如果我们在表达式中想跳出循环,或者提前退出方法它就派上用场了。这们上一篇中也有讲到Expression.Return的用法。当然,我们还可以通过switch case 来返回值,请看下面的switch case的用法。

//简单的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, 而基本上我们用到的地方都是以基类作为参数类型接受的,所以我们可以传递任意类型的表达式。
);
Expression> expr4 = Expression.Lambda>(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类,然后覆盖其中的某一些方法从而达到我们自己的目地。我们要实现什么样的功能呢?

List myUsers = new List();
var userSql = myUsers.AsQueryable().Where(u => u.Age > 2);
Console.WriteLine(userSql);
// SELECT * FROM (SELECT * FROM User) AS T WHERE (Age>2)
List myUsers2 = new List();
var userSql2 = myUsers.AsQueryable().Where(u => u.Name=="Jesse");
Console.WriteLine(userSql2);
// SELECT * FROM (SELECT * FROM USER) AS T WHERE (Name='Jesse')

  我们改造了IQueryable的Where方法,让它根据我们输入的查询条件来构造SQL语句。

  要实现这个功能,首先我们得知道IQueryable的Where 方法在哪里,它是如何实现的?

public static class Queryable
{
public static IQueryable Where(this IQueryable source, Expression> predicate)
{
if (source == null)
{
throw new ArgumentNullException("source");
}
if (predicate == null)
{
throw new ArgumentNullException("predicate");
}
return source.Provider.CreateQuery(
Expression.Call(null, ((MethodInfo)MethodBase.GetCurrentMethod())
.MakeGenericMethod(new Type[] { typeof(TSource) }),
new Expression[] { source.Expression, Expression.Quote(predicate) }));
}
}

  通过F12我们可以跟到System.Linq下有一个Querable的静态类,而我们的Where方法就是是扩展方法的形势存在于这个类中(包括其的GroupBy,Join,Last等有兴趣的同学可以自行Reflect J)。大家可以看到上面的代码中,实际上是调用了Queryable的Provider的CreateQuery方法。这个Provider就是传说中的Linq Provider,但是我们今天不打算细说它,我们的重点在于传给这个方法的参数被转成了一个表达式树。实际上Provider也就是接收了这个表达式树,然后进行遍历解释的,那么我们可以不要Provider直接进行翻译吗? I SAY YES! WHY CAN’T?

public static class QueryExtensions
{
public static string Where(this IQueryable source,
Expression> predicate)
{
var expression = Expression.Call(null, ((MethodInfo)MethodBase.GetCurrentMethod())
.MakeGenericMethod(new Type[] { typeof(TSource) }),
new Expression[] { source.Expression, Expression.Quote(predicate) });
var translator = new QueryTranslator();
return translator.Translate(expression);
}
}

  上面我们自己实现了一个Where的扩展方法,将该Where方法转换成表达式树,只不过我们没有调用Provider的方法,而是直接让另一个类去将它翻译成SQL语句,然后直接返回该SQL语句。接下来的问题是,这个类如何去翻译这个表达式树呢?我们的ExpressionVisitor要全场了!

class QueryTranslator : ExpressionVisitor
{
internal string Translate(Expression expression)
{
this.sb = new StringBuilder();
this.Visit(expression);
return this.sb.ToString();
}
}

  首先我们有一个类继承自ExpressionVisitor,里面有一个我们自己的Translate方法,然后我们直接调用Visit方法即可。上面我们提到了Visit方法实际上是一个入口,会根据表达式的类型调用其它的Visit方法,我们要做的就是找到对应的方法重写就可以了。但是下面有一堆Visit方法,我们要要覆盖哪哪些呢? 这就要看我们的表达式类型了,在我们的Where扩展方法中,我们传入的表达式树是由Expression.Call方法构造的,而它返回的是MethodCallExpression所以我们第一步是覆盖VisitMethodCall。

protected override Expression 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);
return m;
}
throw new NotSupportedException(string.Format("方法{0}不支持", m.Method.Name));
}

  代码很简单,方法名是Where那我们就直接开始拼SQL语句。重点是在这个方法里面两次调用了Visit方法,我们要知道它们会分别调用哪两个具体的Visit方法,我们要做的就是重写它们。

  第一个我们就不说了,大家可以下载源代码自己去调试一下,我们来看看第二个Visit方法。很明显,我们构造了一个Lambda表达式树,但是注意,我们没有直接Visit这Lambda表达式树,它是Visit了它的Body。它的Body是什么?如果我的条件是Age>7,这就是一个二元运算,不是么?所以我们要重写VisitBinary方法,Let’s get started。

protected override Expression VisitBinary(BinaryExpression b)
{
sb.Append("(");
this.Visit(b.Left);
switch (b.NodeType)
{
case ExpressionType.And:
sb.Append(" AND ");
break;
case ExpressionType.Or:
sb.Append(" OR");
break;
case ExpressionType.Equal:
sb.Append(" = ");
break;
case ExpressionType.NotEqual:
sb.Append(" <> ");
break;
case ExpressionType.LessThan:
sb.Append(" <");
break;
case ExpressionType.LessThanOrEqual:
sb.Append(" <= ");
break;
case ExpressionType.GreaterThan:
sb.Append(" > ");
break;
case ExpressionType.GreaterThanOrEqual:
sb.Append(" >= ");
break;
default:
throw new NotSupportedException(string.Format(“二元运算符{0}不支持”, b.NodeType));
}
this.Visit(b.Right);
sb.Append(")");
return b;
}

  我们根据这个表达式的操作类型转换成对应的SQL运算符,我们要做的就是把左边的属性名和右边的值加到我们的SQL语句中。所以我们要重写VisitMember和VisitConstant方法。

protected override Expression VisitConstant(ConstantExpression c)
{
IQueryable q = c.Value as IQueryable;
if (q != null)
{
// 我们假设我们那个Queryable就是对应的表
sb.Append("SELECT * FROM ");
sb.Append(q.ElementType.Name);
}
else if (c.Value == null)
{
sb.Append("NULL");
}
else
{
switch (Type.GetTypeCode(c.Value.GetType()))
{
case TypeCode.Boolean:
sb.Append(((bool)c.Value) ? 1 : 0);
break;
case TypeCode.String:
sb.Append("'");
sb.Append(c.Value);
sb.Append("'");
break;
case TypeCode.Object:
throw new NotSupportedException(string.Format("The constant for '{0}' is not supported", c.Value));
default:
sb.Append(c.Value);
break;
}
}
return c;
}
protected override Expression VisitMember(MemberExpression m)
{
if (m.Expression != null && m.Expression.NodeType == ExpressionType.Parameter)
{
sb.Append(m.Member.Name);
return m;
}
throw new NotSupportedException(string.Format("The member '{0}' is not supported", m.Member.Name));
}

  到这里,我们的来龙去脉基本上就清楚了。来回顾一下我们干了哪些事情。



  1. 重写IQuerable的Where方法,构造MethodCallExpression传给我们的表达式访问类。

  2. 在我们的表达式访问类中重写相应的具体访问方法。

  3. 在具体访问方法中,解释表达式,翻译成SQL语句。

  实际上我们并没有干什么很复杂的事情,只要了解具体的表达式类型和具体表访问方法就可以了。看到很多园友说表达式树难以理解,我也希望能够尽我的努力去把它清楚的表达出来,让大家一起学习,如果大家觉得哪里不清楚,或者说我表述的方式不好理解,也欢迎大家踊跃的提出来,后面我们可以继续完善这个翻译SQL的功能,我们上面的代码中只支持Where语句,并且只支持一个条件。我的目地的希望通过这个例子让大家更好的理解表达式树的遍历问题,这样我们就可以实现我们自己的LinqProvider了,请大家关注,我们来整个Linq To 什么呢?有好点子么? 之间想整个Linq to 博客园,但是好像博客园没有公开Service。

  点这里面下载文中源代码。  

  参考引用:

     http://msdn.microsoft.com/en-us/library/bb397951(v=vs.120).aspx
     http://msdn.microsoft.com/en-us/library/system.linq.expressions.aspx
     http://msdn.microsoft.com/en-us/library/system.linq.expressions.expression.aspx
     http://blogs.msdn.com/b/mattwar/archive/2007/07/30/linq-building-an-iqueryable-provider-part-i.aspx

 

作者:Jesse

出处: http://jesse2013.cnblogs.com/

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。如果觉得还有帮助的话,可以点一下右下角的【推荐】,希望能够持续的为大家带来好的技术文章!想跟我一起进步么?那就【关注】我吧。

【关注】Jesse Liu


原文链接:https://www.cnblogs.com/jesse2013/p/expressiontree-part2.html



推荐阅读
  • 使用Ubuntu中的Python获取浏览器历史记录原文: ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 目录实现效果:实现环境实现方法一:基本思路主要代码JavaScript代码总结方法二主要代码总结方法三基本思路主要代码JavaScriptHTML总结实 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • ZSI.generate.Wsdl2PythonError: unsupported local simpleType restriction ... [详细]
  • EPICS Archiver Appliance存储waveform记录的尝试及资源需求分析
    本文介绍了EPICS Archiver Appliance存储waveform记录的尝试过程,并分析了其所需的资源容量。通过解决错误提示和调整内存大小,成功存储了波形数据。然后,讨论了储存环逐束团信号的意义,以及通过记录多圈的束团信号进行参数分析的可能性。波形数据的存储需求巨大,每天需要近250G,一年需要90T。然而,储存环逐束团信号具有重要意义,可以揭示出每个束团的纵向振荡频率和模式。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • 这是原文链接:sendingformdata许多情况下,我们使用表单发送数据到服务器。服务器处理数据并返回响应给用户。这看起来很简单,但是 ... [详细]
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
author-avatar
lumanman158
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有