表達式樹
“代碼就是數據”是一個很古老的觀念, 其並沒有在很多流行的編程語言中使用. 你可以爭論說所有的.Net程序都使用了這個觀點, 因為JIT將所有IL代碼都認為是數據, 並將他們轉換成基於本機CPU的本地代碼. 這隱藏得很深, 而且由於有存在的庫用於操作IL代碼, 因此它們並沒有被廣泛的應用.
.NET 3.5當中的提供了一種抽象的方式來將代碼展現為一顆對象樹. 這有點類似CodeDOM, 不過是在更高一層的級別上操作, 而且僅限於表達式. 表達式樹的主要用處是在LINQ當中. C# 3當中對於將lambda表達式轉換為表達式樹提供了內建支持. 不過在我們開始講解它們之前先來了解一下在沒有編譯器的幫助之下, 它們是如何適用於.Net Framework當中的.
編程方式構建表達式樹
表達式樹並沒有聽起來的那麼神秘, 雖然有些時候他們的用法看起來的確有點像是魔術. 像它們的名字所展現的, 它們是對象樹, 在樹中的每一個節點都是其內部的一個表達式. 不同類型的表達式代表了可以在代碼中執行的不同操作: 二進制操作, 例如加法; 一元操作, 例如讀取數組長度; 方法調用; 構造器調用等等.
System.Linq.Expression命名空間包含了代表表達式的幾個不同類. 它們全部都繼承自Expression類, Expression是一個抽象的, 並且大部分由創建其他表達式實例的靜態工廠方法組成. Expression類還暴露了兩個屬性:
有很多的類型都繼承了Expression,其中的一部分有很多不同的節點類型(node types): 例如BinaryExpression, 代表了兩個操作數之間的任何操作: 算術, 邏輯, 比較, 數組索引以及其他類似的操作. 這就是為什麼NodeType在這裡這麼重要的原因, 因為它區分了在同一個類裡面不同的表達式類型.
讓我們從一個最簡單的表達式樹開始, 將兩個整數相加. 以下的表達式樹表示2+3:
1: Expression firstArg = Expression.Constant(2);
2: Expression secondArg = Expression.Constant(3);
3: Expression add = Expression.Add(firstArg, secondArg);
4: Console.WriteLine(add);
運行上面的代碼會產生一個”(2+3)”的輸出, 另外還演示了不同的Expression類型還重載了ToString產生更加直觀和利於人類閱讀的輸出. 值得一提的是在代碼中的葉節點將會第一個被創建: 你是從底部往上創建表達式. 這主要是因為表達式是不可變的——只要你創建了一個表達式, 就將永遠無法改變它. 因此你可以將其緩存然後重復使用.
現在我們已經構建了一個表達式樹, 讓我們來嘗試來真正的執行它.
將表達式樹編譯成委托
從Expression繼承的其中一個類就是LambdaExpression. 然後泛型Expression<TDelegate>類又繼承了LambdaExpression, 這看起來容易讓人混淆. Expression和Expression<TDelegate>的不同之處在於Expression<TDelegate>依照參數和返回類型嚴格的指出了表達式是那種類型. 顯而易見, 這是由TDelegate參數類型指定的, 其必須是一個委托類型. 例如, 我們的簡單加法表達式是一個沒有參數並且返回一個int類型的表達式——這與Func<int>匹配, 因為我們可以使用Expression<Func<int>>來以一個靜態類型的方式表示該表達式. 我們可以通過使用Expression.Lambda方法來完成這個工作. 該方法擁有大量的重載——我們例子使用了泛型方法, 該方法使用了一個類型參數指出我們想展示的委托類型.
那麼, 這樣做的目的是什麼呢? LambdaExpression有一個Compile方法, 可以使用它來創建一個適當類型的委托. 現在該委托可以在普通的方式下被執行, 就像是它是用一個普通的方法或者其他方式來創建的一樣. 以下的代碼展示了這種做法:
1: Expression firstArg = Expression.Constant(2);
2: Expression secondArg = Expression.Constant(3);
3: Expression add = Expression.Add(firstArg, secondArg);
4: Func<int> compiled = Expression.Lambda<Func<int>>(add).Compile();
5: Console.WriteLine(compiled());
上述的代碼可能是最費解的用於打印出”5″的方式. 不過同時, 它也是相當令人印象深刻的. 我們以編程方式創建了一些邏輯塊並將它們展現為普通對象, 然後讓框架把所有的東西編譯成”真實的”代碼並且執行它. 你可能永遠不需要像這樣使用表達式樹, 或者完全使用編程方式的來創建它們, 但這對於你了解LINQ是如何工作是非常有用的背景知識. 與CodeDOM不同的是, 表達式樹僅僅只能用於展示單一表達式, 它們並不是被設計用於整個類, 方法甚至是語句(statements), 而且, C#通過lambda表達式直接在語言級別支持表達式樹.