【轉】由淺入深表達式樹(二)遍歷表達式樹

        爲何要學習表達式樹?表達式樹是將咱們原來能夠直接由代碼編寫的邏輯以表達式的方式存儲在樹狀的結構裏,從而能夠在運行時去解析這個樹,而後執行,實現動態的編輯和執行代碼。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

相關文章
相關標籤/搜索