1. 程式人生 > 實用技巧 >由淺入深表達式樹(二)遍歷表示式樹

由淺入深表達式樹(二)遍歷表示式樹

由淺入深表達式樹(二)遍歷表示式樹

  為什麼要學習表示式樹?表示式樹是將我們原來可以直接由程式碼編寫的邏輯以表示式的方式儲存在樹狀的結構裡,從而可以在執行時去解析這個樹,然後執行,實現動態的編輯和執行程式碼。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。