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

  为什么要学习表达式树?表达式树是将我们原来可以直接由代码编写的逻辑以表达式的方式存储在树状的结构里,从而可以在运行时去解析这个树,然后执行,实现动态的编辑和执行代码。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 条评论
登录 后参与评论

相关文章

来自专栏ImportSource

是时候忘掉finalize方法了

故事要从jdk11 early access版说起。 近日,发现jdk11发布了一个早鸟版。心想,jdk10刚刚发布没多久(JDK10要来了:下一代 Java...

3578
来自专栏何俊林

从面试题中看Java的Reference(引用)

前言:四大引用,是一个古老的知识,今天看下 Harlber 授权公众号独家推送的文章, Harlber 的简书地址 :http://www.jianshu.co...

24210
来自专栏听雨堂

字符串处理总结(旧)

在各类应用软件的开发中,字符串操作是最常见的操作之一。在各种不同的数据类型中,字符串类型是和现实世界关联最紧密的。对字符串的读入、比较、拼接、搜索、匹配、替换、...

1878
来自专栏张善友的专栏

Reactive Extensions介绍

Reactive Extensions(Rx)是对LINQ的一种扩展,他的目标是对异步的集合进行操作,也就是说,集合中的元素是异步填充的,比如说从Web或者云端...

1859
来自专栏微信公众号:Java团长

单例模式讨论篇:单例模式与垃圾回收

Jvm的垃圾回收机制到底会不会回收掉长时间不用的单例模式对象,这的确是一个比较有争议性的问题。将这一部分内容单独成篇的目的也是为了与广大博友广泛的讨论一下这个问...

892
来自专栏python3

python time与datetime模块

在python中,与时间处理相关的模块有:time、datetime以及calendar。学会计算时间,对程序的调优非常重要,可以在程序中狂打时间戳,来具体判断...

521
来自专栏用户1191492的专栏

物联网平台设计文档:精简GC(垃圾回收)

许多高级编程语言的自动内存管理功能让编程变成了比较容易的一件事。然而,嵌入式平台经常缺少这一部分功能,这是有原因的:现代垃圾收集(GC)系统使用的...

2465
来自专栏数说工作室

【SAS Says】基础篇:3. 描述数据

本节介绍如何利用SAS写一份数据报告,给出数据的基本信息。 从3.11开始的内容,是留给处女座的,主要说如何用proc tabulate和proc report...

26910
来自专栏铭毅天下

干货 | Elasticsearch5.X Mapping万能模板

0、引言 在关系型数据库如Mysql中,设计库表需要注意的是: 1)需要几个表; 2)每个表有哪些字段; 3)表的主键及外键的设定——便于有效关联。 表的设计遵...

36513
来自专栏余林丰

Effective Java通俗理解(下)

第31条:用实例域代替序数   枚举类型有一个ordinal方法,它范围该常量的序数从0开始,不建议使用这个方法,因为这不能很好地对枚举进行维护,正确应该是利用...

1909

扫描关注云+社区