在.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表達式,看下面的代碼:
<a href="http://11011.net/software/vspaste"></a>
在我們将Lambda表達式指定給Expression<TDelegate>類型的變量(參數)時,編譯器将會發出生成表達式目錄樹的指令,如上面這段代碼中的Lambda表達式(a, b) => a * b + 2将建立一個表達式目錄樹,它表示的是一種資料結構,即我們把一行代碼用資料結構的形式表示了出來,具體來說最終構造出來的表達式目錄樹形狀如下圖所示:
這裡每一個節點都表示一個表達式,可能是一個二進制運算,也可能是一個常量或者參數等,如上圖中的ParameterExpression就是一個參數表達式,ConstantExpression是一個常量表達式,BinaryExpression是一個二進制表達式。我們也可以在Visual Studio中使用Expression Tree Visualizer來檢視該表達式目錄樹:
檢視結果如下圖所示:
<a href="http://images.cnblogs.com/cnblogs_com/Terrylee/WindowsLiveWriter/LINQProvider_12290/TerryLee_0162_2.png"></a>
<a href="http://images.cnblogs.com/cnblogs_com/Terrylee/WindowsLiveWriter/LINQProvider_12290/TerryLee_0161_2.png"></a>
它們都繼承于抽象的基類Expression,而泛型的Expression<TDelegate>則繼承于LambdaExpression。在Expression類中提供了大量的工廠方法,這些方法負責建立以上各種表達式對象,如調用Add()方法将建立一個表示不進行溢出檢查的算術加法運算的BinaryExpression對象,調用Lambda方法将建立一個表示lambda 表達式的LambdaExpression對象,具體提供的方法大家可以查閱MSDN。上面構造表達式目錄樹時我們使用了Lambda表達式,現在我們看一下如何通過這些表達式對象手工構造出一個表達式目錄樹,如下代碼所示:
這裡構造的表達式目錄樹,仍然如下圖所示:
運作這段代碼,看看輸出了什麼:
可以看到,通過手工構造的方式,我們确實構造出了同前面一樣的Lambda表達式。對于一個表達式目錄樹來說,它有幾個比較重要的屬性:
Body:指表達式的主體部分;
Parameters:指表達式的參數;
NodeType:指表達式的節點類型,如在上面的例子中,它的節點類型是Lambda;
Type:指表達式的靜态類型,在上面的例子中,Type為Fun<int,int,int>。
在Expression Tree Visualizer中,我們可以看到表達式目錄樹的相關屬性,如下圖所示:
大家可能經常看到如下這樣的語言,其中第一句是直接用Lambda表達式來初始化了Func委托,而第二句則使用Lambda表達式來構造了一個表達式目錄樹,它們之間的差別是什麼呢?
其實看一下IL就很明顯,其中第一句直接将Lambda表達式直接編譯成了IL,如下代碼所示:
而第二句,由于告訴編譯器是一個表達式目錄樹,是以編譯器會分析該Lambda表達式,并生成表示該Lambda表達式的表達式目錄樹,即它與我們手工建立表達式目錄樹所生成的IL是一緻的,如下代碼所示,此處為了節省空間省略掉了部分代碼:
現在相信大家都看明白了,這裡講解它們的差別主要是為了加深大家對于表達式目錄樹的差別。
前面已經可以構造出一個表達式目錄樹了,現在看看如何去執行表達式目錄樹。我們需要調用Compile方法來建立一個可執行委托,并且調用該委托,如下面的代碼:
運作後輸出的結果:
這裡我們隻要簡單的調用Compile方法就可以了,事實上在.NET Framework中是調用了一個名為ExpressionCompiler的内部類來做表達式目錄樹的執行(注意此處的Compiler不等同于編譯器的編譯)。另外,隻能執行表示Lambda表達式的表達式目錄樹,即LambdaExpression或者Expression<TDelegate>類型。如果表達式目錄樹不是表示Lambda表達式,需要調用Lambda方法建立一個新的表達式。如下面的代碼:
輸出後為:
<a href="http://images.cnblogs.com/cnblogs_com/Terrylee/WindowsLiveWriter/LINQProvider_12290/TerryLee_0164_2.png"></a>
現在我們想要修改表達式目錄樹,讓它表示的Lambda表達式為(a,b)=>(a - (b * 2)),這時就需要編寫自己的表達式目錄樹通路器,如下代碼所示:
使用表達式目錄樹通路器來修改表達式目錄樹,如下代碼所示:
似乎我們是修改表達式目錄樹,其實也不全對,我們隻是修改表達式目錄樹的一個副本而已,因為表達式目錄樹是不可變的,我們不能直接修改表達式目錄樹,看看上面的OperationsVisitor類的實作大家就知道了,在修改過程中複制了表達式目錄樹的節點。
通過前面的介紹,相信大家對于表達式目錄樹已經有些了解了,還有一個很重要的問題,就是為什麼需要表達式目錄樹?在本文開始時,就說過通過解析表達式目錄樹,可以實作我們一些特定的功能,就拿LINQ to SQL為例,看下面這幅圖:
當我們在C#語言中編寫一個查詢表達式時,它将傳回一個IQueryable類型的值,在該類型中包含了兩個很重要的屬性Expression和Provider,如下面的代碼:
我們編寫的查詢表達式,将封裝為一種抽象的資料結構,這個資料結構就是表達式目錄樹,當我們在使用上面傳回的值時,編譯器将會以該值所期望的方式進行翻譯,這種方式就是由Expression和Provider來決定。可以看到,這樣将會非常的靈活且具有良好的可擴充性,有了表達式目錄樹,可以自由的編寫自己的Provider,去查詢我們希望的資料源。經常說LINQ為通路各種不同的資料源提供了一種統一的程式設計方式,其奧秘就在這裡。然而需要注意的是LINQ to Objects并不需要任何特定的LINQ Provider,因為它并不翻譯為表達式目錄樹,後面會說到這一點。
本文轉自lihuijun51CTO部落格,原文連結:http://blog.51cto.com/terrylee/90559 ,如需轉載請自行聯系原作者