為什么要學習表達式樹?表達式樹是將我們原來可以直接由代碼編寫的邏輯以表達式的方式存儲在樹狀的結構里,從而可以在運行時去解析這個樹,然后執行,實現動態的編輯和執行代碼。LINQ to SQL就是通過把表達式樹翻譯成SQL來實現的,所以了解表達樹有助于我們更好的理解 LINQ to SQL,同時如果你有興趣,可以用它創造出很多有意思的東西來。
表達式樹是隨著.NET 3.5推出的,所以現在也不算什么新技術了。但是不知道多少人是對它理解的很透徹, 在上一篇Lambda表達式的回復中就看的出大家對Lambda表達式和表達式樹還是比較感興趣的,那我們就來好好的看一看這個造就了LINQ to SQL以及讓LINQ to Everything的好東西吧。
本系列計劃三篇,第一篇主要介紹表達式樹的創建方式。第二篇主要介紹表達式樹的遍歷問題。第三篇,將利用表達式樹打造一個自己的LinqProvider。
- 由淺入深表達式樹 (一)創建表達式樹
- 由淺入深表達式樹(二)遍歷表達式樹
- 由淺入深表達式樹(三)Linq to 博客園
本文主要內容:
上一篇由淺入深表達式樹(一)我們主要討論了如何根據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)); }
到這里,我們的來龍去脈基本上就清楚了。來回顧一下我們干了哪些事情。
- 重寫IQuerable的Where方法,構造MethodCallExpression傳給我們的表達式訪問類。
- 在我們的表達式訪問類中重寫相應的具體訪問方法。
- 在具體訪問方法中,解釋表達式,翻譯成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
文章列表