C#3.0新增功能10 表達式樹 02 說明

  • 2019 年 10 月 4 日
  • 筆記

  表達式樹是定義代碼的數據結構。 它們基於編譯器用於分析代碼和生成已編譯輸出的相同結構。表達式樹和 Roslyn API 中用於生成分析器和 CodeFixes 的類型之間存在很多相似之處。 (分析器和 CodeFixes 是 NuGet 包,用於對代碼執行靜態分析,並可為開發人員建議可能的修補程序。)兩者概念相似,且最終結果是一種數據結構,該結構允許以有意義的方式對源代碼進行檢查。 但是,表達式樹基於一組與 Roslyn API 完全不同的類和 API。

讓我們來舉一個簡單的示例。 以下是一個代碼行:

var sum = 1 + 2;

如果要將其作為一個表達式樹進行分析,則該樹包含多個節點。 最外面的節點是具有賦值 (var sum = 1 + 2;) 的變量聲明語句,該節點包含若干子節點:變量聲明、賦值運算符和一個表示等於號右側的表達式。 該表達式被進一步細分為表示加法運算、該加法左操作數和右操作數的表達式。

讓我們稍微深入了解一下構成等於號右側的表達式。 該表達式是 1 + 2。 這是一個二進制表達式。 更具體地說,它是一個二進制加法表達式。 二進制加法表達式有兩個子表達式,表示加法表達式的左側和右側節點。 此處的兩個節點都是常量表達式:左操作數是值 1,右操作數是值 2

直觀地看,整個語句是一個樹:應從根節點開始,遍歷到樹中的每個節點,以查看構成語句的代碼:

  • 具有賦值 (var sum = 1 + 2;) 的變量聲明語句
    • 隱式變量類型聲明 (var sum)賦值運算符 (=)
      • 隱式 var 關鍵字 (var)
      • 變量名稱聲明 (sum)
    • 二進制加法表達式 (1 + 2)
      • 左操作數 (1)
      • 加法運算符 (+)
      • 右操作數 (2)

這可能看起來很複雜,但它功能強大。 按照相同的過程,可以分解更加複雜的表達式。 請思考此表達式:

var finalAnswer = this.SecretSauceFunction(currentState.createInterimResult(),                                             currentState.createSecondValue(1, 2),                                             decisionServer.considerFinalOptions("hello")                                             )                    + MoreSecretSauce('A', DateTime.Now, true);

上述表達式也是具有賦值的變量聲明。 在此情況下,賦值的右側是一棵更加複雜的樹。 我不打算分解此表達式,但請思考一下不同的節點可能是什麼。 存在使用當前對象作為接收方的方法調用,其中一個調用具有顯式 this 接收方,一個調用不具有此接收方。 存在使用其他接收方對象的方法調用,存在不同類型的常量參數。 最後,存在二進制加法運算符。 該二進制加法運算符可能是對重寫的加法運算符的方法調用(具體取決於 SecretSauceFunction()MoreSecretSauce() 的返回類型),解析為對為類定義的二進制加法運算符的靜態方法調用。

儘管具有這種感知上的複雜性,但上面的表達式創建了一種樹形結構,可以像第一個示例那樣輕鬆地導航此結構。 可以保持遍歷子節點,以查找表達式中的葉節點。 父節點將具有對其子節點的引用,且每個節點均具有一個用於介紹節點類型的屬性。

表達式樹的結構非常一致。 了解基礎知識後,你甚至可以理解以表達式樹形式表示的最複雜的代碼。 優美的數據結構說明了 C# 編譯器如何分析最複雜的 C# 程序並從該複雜的源代碼創建正確的輸出。

熟悉表達式樹的結構後,你會發現通過快速獲得的知識,你可處理許多越來越高級的方案。 表達式樹的功能非常強大。

除了轉換算法以在其他環境中執行之外,表達式樹還可用於在執行代碼前輕鬆編寫檢查代碼的算法。 可以編寫參數為表達式的方法,然後在執行代碼之前檢查這些表達式。 表達式樹是代碼的完整表示形式:可以看到任何子表達式的值。 可以看到方法和屬性名稱。 可以看到任何常數表達式的值。 還可以將表達式樹轉換為可執行的委託,並執行代碼。

通過表達式樹的 API,可創建表示幾乎任何有效代碼構造的樹。 但是,出於儘可能簡化的考慮,不能在表達式樹中創建某些 C# 習慣用語。 其中一個示例就是異步表達式(使用 asyncawait關鍵字)。 如果需要異步算法,則需要直接操作 Task 對象,而不是依賴於編譯器支持。 另一個示例是創建循環。 通常,通過使用 forforeachwhiledo 循環對其進行創建。 正如稍後可以在本系列中看到的那樣,表達式樹的 API 支持單個循環表達式,該表達式包含控制重複循環的 breakcontinue 表達式。

不能執行的操作是修改表達式樹。 表達式樹是不可變的數據結構。 如果想要改變(更改)表達式樹,則必須創建基於原始樹副本但包含所需更改的新樹。