Expression Tree 學習筆記(一)


大家可能都知道Expression Tree是.NET 3.5引入的新增功能。不少朋友們已經聽說過這一特性,但還沒來得及了解。看看博客園里的老趙等諸多牛人,將Expression Tree玩得眼花繚亂,是否常常覺得有點落伍了呢?其實Expression Tree是一個一點就透的特性,只要對其基本概念有了一定的了解,就可以自己發揮出無數的用法。特別是之前對Reflection,泛型等知識有過一些了解的話,就會發現Expression Tree的加入絕對是你工作中的得力助手。如果你是Expression Tree的新手,那么從本文開始,你就可以領略這一工具,之后再看老趙的文章就從容不迫了~

從表達式說起

Expression Tree從名稱上看就是“表達式樹”的意思。許多人一看到它,就會想起Lambda表達式,委托,Linq等等一堆名詞。但其實最基本的概念就是“表達式”。現在讓我們把那些名詞全都給忘了,來重新了解一下表達式。

表達式是當今編程語言中最重要的組成成分。簡單的說,表達式就是變量、數值、運算符、函數組合起來,表示一定意義的式子。例如下面這些都是(C#)的表達式:

3 //常數表達式
a //變量或參數表達式
!a //一元邏輯非表達式
a + b //二元加法表達式
Math.Sin(a) //方法調用表達式
new StringBuilder() //new 表達式

此外還有取地址表達式,新建數組表達式,賦值表達式等許多種。如你所見,表達式常常能夠表示一個值或對象,因此在C#這類強類型語言里,表達式常常有一個相應的類型。例如“3”這個表達式就是int類型的。不過有時表達式也沒有值,例如方法調用表達式,如果方法沒有返回值的話這個表達式也就沒有值。這種情況我們也說表達式的類型是void。

表達式的一個重要的特點就是它可以無限地組合,只要符合正確的類型和語義。例如+可以用於各類數值類型的加法,那么加號的左右就可以是任何類型為相應數值的表達式。可以是函數調用和常數:Math.Sin(a) + 3;也可以是同樣的加法表達式a + 2 + 3。想必大家在實踐中早就用上這個特性了。那么a + 2 + 3是如何計算出正確的值來的呢?應該首先計算(a + 2)的結果b,然后計算b + 3的值。如果我們用一個圖來表示這個過程,它就像這樣:

 image

同理,表達式Math.Sin(a) + 3也可以表示成這樣:

 image

如你所見,所有表達式都可以表示成這種樹一樣的結構。每個節點和它所有的后裔都構成一個獨立的表達式。如果我們將表達式表示成這種結構,就可以輕易地明白它的運算規則和步驟。因此我們可以用一種樹狀的數據結構來表示每一個表達式。這個數據結構就是表達式樹。

表達式樹

剛才提到了,表達式樹就是一種表示表達式的數據結構。System.Linq.Expression命名空間下的Expression類和它的諸多子類就是這一數據結構的實現。每個表達式都可以表示成Expression某個子類的實例。每個Expression子類都按照相應表達式的特點儲存自己的子節點。例如BinaryExpression就表示各種二元運算符的表達式。它的Left和Right屬性就是參與二元運算的兩個運算數。下面開始我們將每種表達式的內部特定結構稱作表達式的“成分”。比如二元運算表達式的成分就是左運算數表達式、右運算符表達式和一個運算符。

Expression各個子類的構造函數都是不公開的,要創建表達式樹只能使用Expression類提供的靜態方法。(這同時也說明表達式樹體系是不能自己擴展的)如果我們要創建1 + 2 + 3這個表達式的表達式樹,可以這樣寫:

ConstantExpression exp1 = Expression.Constant(1);
ConstantExpression exp2 = Expression.Constant(2);

BinaryExpression exp12 = Expression.Add(exp1, exp2);

ConstantExpression exp3 = Expression.Constant(3);

BinaryExpression exp123 = Expression.Add(exp12, exp3);

這個應該非常好理解。下面如果我們想寫出Math.Sin(a)這個表達式的表達式樹怎么辦呢?這時問題就來了,這里面的“a”不知道該用什么表示。為了解決這個問題,下面Lambda表達式該登場了。

Lambda表達式

Lambda也是C#3.0/VB9新引入的表達式。我們都知道它和以前的匿名函數和委托有關。不過現在還是把這些暫時都忘掉,完全把Lambda表達式當成一種新的表達式來看到。剛才我們看到了各種各樣的表達式,有的表示一個常數;有的表示一個變量;有的表示加法;有的表示函數調用等等。Lambda表達式作為一個表達式,它表達的是一個函數。Lambda表達式的成分就是一系列的參數加上一個表示函數邏輯的表達式組成。

(parameters) => expression

Lambda表達式最重要的特色是它可以引入一批參數,這批參數可以在函數體表達式中使用。基於這種特色,我們就可以創建出帶自定義變量的表達式樹,而這些自定義變量就表示成Lambda表達式的參數:

ParameterExpression expA = Expression.Parameter(typeof(double), "a"); //參數a
MethodCallExpression expCall = Expression.Call(null, 
    typeof(Math).GetMethod("Sin", BindingFlags.Static | BindingFlags.Public), 
    expA); //Math.Sin(a)

LambdaExpression exp = Expression.Lambda(expCall, expA); // a => Math.Sin(a)

我們這里使用了一個新的Expression樹節點——MethodCallExpression。它可以表示一次方法調用。方法是使用MethodInfo實例來表示的。如果畫成圖的話,Lambda表達式可以畫成這樣:

image

 

由此可見,用Lambda表達式表示函數是一個非常直觀的過程。有時候我們真的覺得沒有名字的函數才是真正的函數。因為函數只需要參數和函數體兩個成分即可,名稱只是為了在別處引用它才需要的。

到此為止,我們已經理解了表達式樹的基本概念。但是我們還只能用最原始的方法一步一步地構建表達式樹。前面我們用到的LambdaExpression是適用於各種類型函數的類,.NET還提供了一種適用於特定委托類型的LambdaExpression<TDelegate>類型。我們用它來表示強類型的LambdaExpression。現在我們就要引入C#、VB真正表達式和表達式樹之間的橋梁——表達式樹字面量(Expression Tree Literals),可以自動從Lambda表達式生成它的表達式樹

Expression<Func<double, double>> exp = a => Math.Sin(a);

注意這個賦值語句,左側是一個強類型的LambdaExpression:Expression<Func<double, double>>,右側是一個真正的C#語法的Lambda表達式。C#的編譯器在這種情況下就能自動為你生成右側Lambda表達式的表達式樹。也就是說,這個exp和我們剛才手工生成Lambda表達式樹基本是一樣的。注意,這種特殊的語法僅能從Lambda表達式獲得表達式樹。別的表達式是不能自動生成表達式樹的。但是一旦我們獲得了Lambda表達式,就可以直接從它的子節點獲得內部表達式了。這是一個非常有用的語法,要深刻理解它的作用。

需要注意的是,這里的委托類型Func<double, double>有雙重作用,首先它限定生成的表達式樹是一個接受double,並返回double的一元Lambda函數;其次這個委托可以直接用在Lambda表達式樹的編譯當中,可在C#作強類型處理。我們后面談到表達式樹的編譯時再詳細的討論這個問題。

表達式樹的意義:數據化的表達式

我們現在已經能夠用兩種方式創建表達式樹——用Expression的節點組合或者直接從C#、VB的Lambda表達式生成。不管使用的是那種方法,最后我們得到的是一個內存中樹狀結構的數據。如果我們願意,可以將它還原成文本源代碼的表達式或者序列化到字符串里。注意,如果是C#的表達式本身,我們是沒法對它進行輸出或者序列化的,它只存在於編譯之前的源文件里。現在的表達式樹已經成為了一種數據,而不在是語言中的表達式。我們可以在運行的時候處理這一數據,精確了解其內在邏輯;將它傳遞給其他的程序或者再次編譯成為可以執行的代碼。這就可以總結為表達式樹的三大基本用途:

  • 運行時分析表達式的邏輯
  • 序列化或者傳輸表達式
  • 重新編譯成可執行的代碼

在下一篇中,我們將着重介紹這三者在實際開發中的用途。

習題

還有習題?別擔心,你可以將下列問題當做上機實踐的素材,以便很快地理解本次學到的內容。

第一題:畫出下列表達式的表達式樹。一開始,您很可能不知道某些操作其實也是表達式(比如取數組的運算符a[2]),不過沒有關系,后面的習題將幫你驗證這一點。

-a

a + b * 2

Math.Sin(x) + Math.Cos(y)

new StringBuilder(“Hello”)

new int[] { a, b, a + b}

a[i – 1] * i

a.Length > b | b >= 0

(高難度)new System.Windows.Point() { X = Math.Sin(a), Y = Math.Cos(a) }

提示:注意運算符的優先級。倒數第二題的a是String類型,其余變量你可以用任意合適的簡單類型。如果想知道以上表達式分別是什么表達式,可以查MSDN。

第二題:將上述表達式中的變量提取成參數,表示成Lambda表達式的形式。然后用Expression靜態方法逐漸組合的方式將他們構建出來。

例如a + b * 2寫成Lambda表達式就成了(int a, int b)=> a + b * 2。按照前面Math.Sin(a)例子的做法用Expression的方法組合出這一邏輯。

第三題:驗證您第二題的結果。請將生成Expression實例ToString(),它就會顯示出它的表達式原型。看看您構建的表達式ToString()出來是不是正確。

如果您發現生成的Expression不是你想要構建的,又不知道該怎么做的話,可以用表達式樹字面量的語法讓C#編譯器幫您生成。然后用Reflector反編譯它就能看到正確的表達式樹。不過C#編譯器有時會使用一些作弊手法,聰明的你應該能找到繞過的手段……

原文鏈接:http://www.cnblogs.com/Ninputer/archive/2009/08/28/expression_tree1.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM