概述
在.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: nop
IL_0001: ldsfld class [System.Core] System.Func`3<int32,int32,int32>
TerryLee.LinqToLiveSearch.Program::
'CS$<>9__CachedAnonymousMethodDelegate1'
IL_0006: brtrue.s IL_001b
IL_0008: ldnull
IL_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_001b
IL_001b: ldsfld class [System.Core] System.Func`3<int32,int32,int32>
TerryLee.LinqToLiveSearch.Program::
'CS$<>9__CachedAnonymousMethodDelegate1'
IL_0020: stloc.0
IL_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: nop
IL_0001: ldtoken [mscorlib]System.Int32
IL_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.2
IL_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打下一個基礎,希望對於大家有所幫助。