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

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

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

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

  本文主要内容:

  • 有返回值的表达式树示例
  • 通过表达式树访问类翻译SQL查询Where语句

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

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

有返回值的表达式树

// 直接返回常量值 
ConstantExpression ce1 = Expression.Constant(10);
            
// 直接用我们上面创建的常量表达式来创建表达式树
Expression<Func<int>> expr1 = Expression.Lambda<Func<int>>(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<Func<int>> expr2 = Expression.Lambda<Func<int>>(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<Func<int,int>> expr3 = Expression.Lambda<Func<int,int>>(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<Func<int, string>> expr4 = Expression.Lambda<Func<int, 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类,然后覆盖其中的某一些方法从而达到我们自己的目地。我们要实现什么样的功能呢?

List<User> myUsers = new List<User>();
var userSql = myUsers.AsQueryable().Where(u => u.Age > 2);
Console.WriteLine(userSql);
// SELECT * FROM (SELECT * FROM User) AS T WHERE (Age>2)

List<User> myUsers2 = new List<User>();
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<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate)
    {
        if (source == null)
        {
            throw new ArgumentNullException("source");
        }
        if (predicate == null)
        {
            throw new ArgumentNullException("predicate");
        }

        return source.Provider.CreateQuery<TSource>(
            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<TSource>(this IQueryable<TSource> source,
        Expression<Func<TSource, bool>> 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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏技术栈大杂烩

Python: 链式赋值的坑

可能你会觉得我又要说什么变量赋值就是引用, 这么简单的知识就不讨论啦, 相信聪明的大家肯定都知道的, 我想讲的是链式赋值

591
来自专栏恰同学骚年

数据结构基础温故-1.线性表(下)

在上一篇中,我们了解了单链表与双链表,本次将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循...

782
来自专栏大内老A

ASP.NET MVC Controller激活系统详解:IoC的应用[下篇]

[上篇]除了通过自定义ControllerFactory的方式引入IoC之外,在使用默认DefaultControllerFactory情况下也可以通过一些扩展...

17610
来自专栏小樱的经验随笔

洛谷 P1177 【模板】快速排序【13种排序模版】

P1177 【模板】快速排序 题目描述 利用快速排序算法将读入的N个数从小到大排序后输出。 快速排序是信息学竞赛的必备算法之一。对于快速排序不是很了解的同学可以...

3154
来自专栏北京马哥教育

深度详解 Python yield与实现

学Python最简单的方法是什么?推荐阅读:Python开发工程师成长魔法 Python yield与实现 yield的功能类似于return,但是不同之处在于...

43212
来自专栏冰霜之地

iOS 如何实现Aspect Oriented Programming (下)

从调用栈可以看出,Aspects hook过程主要分4个阶段,hookClass,ASPECTS_ARE_BEING_CALLED,prepareClassAn...

652
来自专栏Janti

springboot之使用redistemplate优雅地操作redis

6033
来自专栏熊二哥

JDK1.8快速入门

JDK8提供了非常多的便捷用法和语法糖,其编码效率几乎接近于C#开发,maven则是java目前为止最赞的jar包管理和build工具,这两部分内容都不算多,就...

1879
来自专栏java架构师

Stream篇(1)

最近在看一个写的很好的博客,为了加深记忆,把自认为重要的东西,一边看,一边记在这里 一、什么是流、字节序列、字节 一条河中有一条鱼游过,这条鱼就是一个字节,这个...

2837
来自专栏跟着阿笨一起玩NET

SQL字符串的分组聚合(ZT)

    今天在看订阅的RSS的时候,看到这么一个问题:T-Sql中如何对分组的信息进行聚合,并以逗号连接字符;也就是对一个表中的某个字段进行分组,然后对另一个字...

401

扫码关注云+社区