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

        为何要学习表达式树?表达式树是将咱们原来能够直接由代码编写的逻辑以表达式的方式存储在树状的结构里,从而能够在运行时去解析这个树,而后执行,实现动态的编辑和执行代码。LINQ to SQL就是经过把表达式树翻译成SQL来实现的,因此了解表达树有助于咱们更好的理解 LINQ to SQL,同时若是你有兴趣,能够用它创造出不少有意思的东西来。html

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

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

  本文主要内容:学习

  上一篇由浅入深表达式树(一)咱们主要讨论了如何根据Lambda表达式以及经过代码的方式直接建立表达式树。表达式树主要是由不一样类型的表达式构成的,而在上文中咱们也列出了比较经常使用的几种表达式类型,因为它自己结构的特色因此用代码写起来然免有一点繁琐,固然咱们也不必定要从头至尾彻底本身去写,只有咱们理解它了,咱们才能更好的去使用它。ui

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

有返回值的表达式树

// 直接返回常量值 
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官方能找到的关于遍历表达式树的文章真的很少,有一篇比较全的(连接),真的没有办法看下去。请问盖茨叔叔就是这样教大家写文档的么?orm

  可是ExpressionVisitor是惟一一种咱们能够拿来就用的帮助类,因此咱们硬着头皮也得把它啃下去。咱们能够看一下ExpressionVisitor类的主要入口方法是Visit方法,其中主要是一个针对ExpressionNodeType的switch case,这个包含了85种操做类型的枚举类,可是不用担忧,在这里咱们只处理44种操做类型,14种具体的表达式类型,也就是说只有14个方法咱们须要区别一下。我将上面连接中的代码转换成下面的表格方便你们查阅。htm

  认识了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

相关文章
相关标签/搜索