[转]打造自己的LINQ Provider(上):Expression Tree揭秘
概述
在.NET Framework 3.5中提供了LINQ 支持后,LINQ就以其強大而優雅的編程方式贏得了開發人員的喜愛,而各種LINQ Provider更是滿天飛,如LINQ to NHibernate、LINQ to Google等,大有“一切皆LINQ”的趨勢。LINQ本身也提供了很好的擴展性,使得我們可以輕松的編寫屬于自己的LINQ Provider。
本文為打造自己的LINQ Provider系列文章第一篇,主要介紹表達式目錄樹(Expression Tree)的相關知識。
認識表達式目錄樹
究竟什么是表達式目錄樹(Expression Tree),它是一種抽象語法樹或者說它是一種數據結構,通過解析表達式目錄樹,可以實現我們一些特定的功能(后面會說到),我們首先來看看如何構造出一個表達式目錄樹,最簡單的方法莫過于使用Lambda表達式,看下面的代碼:
Expression<Func<int, int, int>> expression = (a, b) => a * b + 2;在我們將Lambda表達式指定給Expression<TDelegate>類型的變量(參數)時,編譯器將會發出生成表達式目錄樹的指令,如上面這段代碼中的Lambda表達式(a, b) => a * b + 2將創建一個表達式目錄樹,它表示的是一種數據結構,即我們把一行代碼用數據結構的形式表示了出來,具體來說最終構造出來的表達式目錄樹形狀如下圖所示:
這里每一個節點都表示一個表達式,可能是一個二元運算,也可能是一個常量或者參數等,如上圖中的ParameterExpression就是一個參數表達式,ConstantExpression是一個常量表達式,BinaryExpression是一個二元表達式。我們也可以在Visual Studio中使用Expression Tree Visualizer來查看該表達式目錄樹:
查看結果如下圖所示:
這里說一句,Expression Tree Visualizer可以從MSDN Code Gallery上的LINQ Sample中得到。現在我們知道了表達式目錄樹的組成,來看看.NET Framework到底提供了哪些表達式?如下圖所示:
它們都繼承于抽象的基類Expression,而泛型的Expression<TDelegate>則繼承于LambdaExpression。在Expression類中提供了大量的工廠方法,這些方法負責創建以上各種表達式對象,如調用Add()方法將創建一個表示不進行溢出檢查的算術加法運算的BinaryExpression對象,調用Lambda方法將創建一個表示lambda 表達式的LambdaExpression對象,具體提供的方法大家可以查閱MSDN。上面構造表達式目錄樹時我們使用了Lambda表達式,現在我們看一下如何通過這些表達式對象手工構造出一個表達式目錄樹,如下代碼所示:
static void Main(string[] args) {ParameterExpression paraLeft = Expression.Parameter(typeof(int), "a");ParameterExpression paraRight = Expression.Parameter(typeof(int), "b");BinaryExpression binaryLeft = Expression.Multiply(paraLeft, paraRight);ConstantExpression conRight = Expression.Constant(2, typeof(int));BinaryExpression binaryBody = Expression.Add(binaryLeft, conRight);LambdaExpression lambda = Expression.Lambda<Func<int, int, int>>(binaryBody, paraLeft, paraRight);Console.WriteLine(lambda.ToString());Console.Read(); }這里構造的表達式目錄樹,仍然如下圖所示:
運行這段代碼,看看輸出了什么:
可以看到,通過手工構造的方式,我們確實構造出了同前面一樣的Lambda表達式。對于一個表達式目錄樹來說,它有幾個比較重要的屬性:
Body:指表達式的主體部分;
Parameters:指表達式的參數;
NodeType:指表達式的節點類型,如在上面的例子中,它的節點類型是Lambda;
Type:指表達式的靜態類型,在上面的例子中,Type為Fun<int,int,int>。
在Expression Tree Visualizer中,我們可以看到表達式目錄樹的相關屬性,如下圖所示:
表達式目錄樹與委托
大家可能經常看到如下這樣的語言,其中第一句是直接用Lambda表達式來初始化了Func委托,而第二句則使用Lambda表達式來構造了一個表達式目錄樹,它們之間的區別是什么呢?
static void Main(string[] args) {Func<int, int, int> lambda = (a, b) => a + b * 2;Expression<Func<int, int, int>> expression = (a, b) => a + b * 2; }其實看一下IL就很明顯,其中第一句直接將Lambda表達式直接編譯成了IL,如下代碼所示:
.method private hidebysig static void Main(string[] args) cil managed {.entrypoint.maxstack 3.locals init ([0] class [System.Core]System.Func`3<int32,int32,int32> lambda)IL_0000: nopIL_0001: ldsfld class [System.Core]System.Func`3<int32,int32,int32> TerryLee.LinqToLiveSearch.Program::'CS$<>9__CachedAnonymousMethodDelegate1'IL_0006: brtrue.s IL_001bIL_0008: ldnullIL_0009: ldftn int32 TerryLee.LinqToLiveSearch.Program::'<Main>b__0'(int32,int32)IL_000f: newobj instance void class [System.Core]System.Func`3<int32,int32,int32>::.ctor(object,native int)IL_0014: stsfld class [System.Core]System.Func`3<int32,int32,int32> TerryLee.LinqToLiveSearch.Program::'CS$<>9__CachedAnonymousMethodDelegate1'IL_0019: br.s IL_001bIL_001b: ldsfld class [System.Core]System.Func`3<int32,int32,int32> TerryLee.LinqToLiveSearch.Program::'CS$<>9__CachedAnonymousMethodDelegate1'IL_0020: stloc.0IL_0021: ret }而第二句,由于告訴編譯器是一個表達式目錄樹,所以編譯器會分析該Lambda表達式,并生成表示該Lambda表達式的表達式目錄樹,即它與我們手工創建表達式目錄樹所生成的IL是一致的,如下代碼所示,此處為了節省空間省略掉了部分代碼:
.method private hidebysig static void Main(string[] args) cil managed {.entrypoint.maxstack 4.locals init ([0] class [System.Core]System.Linq.Expressions.Expression`1<class [System.Core]System.Func`3<int32,int32,int32>> expression,[1] class [System.Core]System.Linq.Expressions.ParameterExpression CS$0$0000,[2] class [System.Core]System.Linq.Expressions.ParameterExpression CS$0$0001,[3] class [System.Core]System.Linq.Expressions.ParameterExpression[] CS$0$0002)IL_0000: nopIL_0001: ldtoken [mscorlib]System.Int32IL_0006: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(...)IL_000b: ldstr "a"IL_0010: call class [System.Core]System.Linq.Expressions.ParameterExpression [System.Core]System.Linq.Expressions.Expression::Parameter(class [mscorlib]System.Type,IL_0038: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle()IL_003d: call class [System.Core]System.Linq.Expressions.ConstantExpression [System.Core]System.Linq.Expressions.Expression::Constant(object,class [mscorlib]System.Type)IL_0042: call class [System.Core]System.Linq.Expressions.BinaryExpression [System.Core]System.Linq.Expressions.Expression::Multiply(class [System.Core]System.Linq.Expressions.Expression,class [System.Core]System.Linq.Expressions.Expression)IL_0047: call class [System.Core]System.Linq.Expressions.BinaryExpression[System.Core]System.Linq.Expressions.Expression::Add(class [System.Core]System.Linq.Expressions.Expression,class [System.Core]System.Linq.Expressions.Expression)IL_004c: ldc.i4.2IL_004d: newarr [System.Core]System.Linq.Expressions.ParameterExpression }現在相信大家都看明白了,這里講解它們的區別主要是為了加深大家對于表達式目錄樹的區別。
執行表達式目錄樹
前面已經可以構造出一個表達式目錄樹了,現在看看如何去執行表達式目錄樹。我們需要調用Compile方法來創建一個可執行委托,并且調用該委托,如下面的代碼:
static void Main(string[] args) {ParameterExpression paraLeft = Expression.Parameter(typeof(int), "a");ParameterExpression paraRight = Expression.Parameter(typeof(int), "b");BinaryExpression binaryLeft = Expression.Multiply(paraLeft, paraRight);ConstantExpression conRight = Expression.Constant(2, typeof(int));BinaryExpression binaryBody = Expression.Add(binaryLeft, conRight);Expression<Func<int, int, int>> lambda = Expression.Lambda<Func<int, int, int>>(binaryBody, paraLeft, paraRight);Func<int, int, int> myLambda = lambda.Compile();int result = myLambda(2, 3);Console.WriteLine("result:" + result.ToString());Console.Read(); }運行后輸出的結果:
這里我們只要簡單的調用Compile方法就可以了,事實上在.NET Framework中是調用了一個名為ExpressionCompiler的內部類來做表達式目錄樹的執行(注意此處的Compiler不等同于編譯器的編譯)。另外,只能執行表示Lambda表達式的表達式目錄樹,即LambdaExpression或者Expression<TDelegate>類型。如果表達式目錄樹不是表示Lambda表達式,需要調用Lambda方法創建一個新的表達式。如下面的代碼:
static void Main(string[] args) {BinaryExpression body = Expression.Add(Expression.Constant(2),Expression.Constant(3));Expression<Func<int>> expression = Expression.Lambda<Func<int>>(body, null);Func<int> lambda = expression.Compile();Console.WriteLine(lambda()); }訪問與修改表達式目錄樹
在本文一開始我就說過, 通過解析表達式目錄樹,我們可以實現一些特定功能,既然要解析表達式目錄樹,對于表達式目錄樹的訪問自然是不可避免的。在.NET Framework中,提供了一個抽象的表達式目錄樹訪問類ExpressionVisitor,但它是一個internal的,我們不能直接訪問。幸運的是,在MSDN中微軟給出了ExpressionVisitor類的實現,我們可以直接拿來使用。該類是一個抽象類,微軟旨在讓我們在集成ExpressionVisitor的基礎上,實現自己的表達式目錄樹訪問類。現在我們來看簡單的表達式目錄樹:
static void Main(string[] args) {Expression<Func<int, int, int>> lambda = (a, b) => a + b * 2;Console.WriteLine(lambda.ToString()); }輸出后為:
現在我們想要修改表達式目錄樹,讓它表示的Lambda表達式為(a,b)=>(a - (b * 2)),這時就需要編寫自己的表達式目錄樹訪問器,如下代碼所示:
public class OperationsVisitor : ExpressionVisitor {public Expression Modify(Expression expression){return Visit(expression);}protected override Expression VisitBinary(BinaryExpression b){if (b.NodeType == ExpressionType.Add){Expression left = this.Visit(b.Left);Expression right = this.Visit(b.Right);return Expression.Subtract(left,right);}return base.VisitBinary(b);} }使用表達式目錄樹訪問器來修改表達式目錄樹,如下代碼所示:
static void Main(string[] args) {Expression<Func<int, int, int>> lambda = (a, b) => a + b * 2;var operationsVisitor = new OperationsVisitor();Expression modifyExpression = operationsVisitor.Modify(lambda);Console.WriteLine(modifyExpression.ToString()); }運行后可以看到輸出:
似乎我們是修改表達式目錄樹,其實也不全對,我們只是修改表達式目錄樹的一個副本而已,因為表達式目錄樹是不可變的,我們不能直接修改表達式目錄樹,看看上面的OperationsVisitor類的實現大家就知道了,在修改過程中復制了表達式目錄樹的節點。
為什么需要表達式目錄樹
通過前面的介紹,相信大家對于表達式目錄樹已經有些了解了,還有一個很重要的問題,就是為什么需要表達式目錄樹?在本文開始時,就說過通過解析表達式目錄樹,可以實現我們一些特定的功能,就拿LINQ to SQL為例,看下面這幅圖:
當我們在C#語言中編寫一個查詢表達式時,它將返回一個IQueryable類型的值,在該類型中包含了兩個很重要的屬性Expression和Provider,如下面的代碼:
我們編寫的查詢表達式,將封裝為一種抽象的數據結構,這個數據結構就是表達式目錄樹,當我們在使用上面返回的值時,編譯器將會以該值所期望的方式進行翻譯,這種方式就是由Expression和Provider來決定。可以看到,這樣將會非常的靈活且具有良好的可擴展性,有了表達式目錄樹,可以自由的編寫自己的Provider,去查詢我們希望的數據源。經常說LINQ為訪問各種不同的數據源提供了一種統一的編程方式,其奧秘就在這里。然而需要注意的是LINQ to Objects并不需要任何特定的LINQ Provider,因為它并不翻譯為表達式目錄樹,后面會說到這一點。
原文鏈接:打造自己的LINQ Provider(上):Expression Tree揭秘
作者:李會軍.
總結
以上是生活随笔為你收集整理的[转]打造自己的LINQ Provider(上):Expression Tree揭秘的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 做梦梦到别人家办白事什么预兆
- 下一篇: spring-test的简单实用方式