表達樹

表達樹簡介

我們來自哪裡

表示式樹都是關於在執行時消耗原始碼。考慮一種計算銷售訂單 decimal CalculateTotalTaxDue(SalesOrder order) 的銷售稅的方法。在 .NET 程式中使用該方法很簡單 - 你只需將其稱為 decimal taxDue = CalculateTotalTaxDue(order);。如果要將其應用於遠端查詢(SQL,XML,遠端伺服器等)的所有結果,該怎麼辦?那些遠端查詢源無法呼叫該方法! 傳統上,在所有這些情況下,你都必須反轉流量。建立整個查詢,將其儲存在記憶體中,然後遍歷結果並計算每個結果的稅。

如何避免流量反轉的記憶體和延遲問題

表示式樹是樹格式的資料結構,其中每個節點都包含一個表示式。它們用於轉換表示式中的編譯指令(如用於過濾資料的方法),這些指令可以在程式環境之外使用,例如在資料庫查詢中。

這裡的問題是遠端查詢無法訪問我們的方法。我們可以避免這個問題,相反,我們將方法的指令傳送到遠端查詢。在我們的 CalculateTotalTaxDue 示例中,這意味著我們傳送此資訊:

  1. 建立一個變數來儲存總稅額
  2. 迴圈遍歷訂單上的所有行
  3. 對於每一行,檢查產品是否應納稅
  4. 如果是,則將匯流排乘以適用的稅率,並將該金額新增到總額中
  5. 否則什麼也不做

使用這些指令,遠端查詢可以在建立資料時執行工作。

實現這一點有兩個挑戰。如何將已編譯的 .NET 方法轉換為指令列表,以及如何以遠端系統可以使用的方式格式化指令?

沒有表示式樹,你只能解決 MSIL 的第一個問題。 (MSIL 是由 .NET 編譯器建立的類似彙編程式的程式碼。)解析 MSIL 是可能的,但這並不容易。即使你正確解析它,也很難確定原始程式設計師對特定例程的意圖。

表達樹節省了一天

表示式樹解決了這些確切問題。它們表示程式指令樹資料結構,其中每個節點代表一條指令,並且引用了執行該指令所需的所有資訊。例如,MethodCallExpression 參考 1)它要呼叫的 MethodInfo,2)它將傳遞給該方法的 Expressions 列表,3)例如方法,你將呼叫方法的 Expression。你可以走樹並應用遠端查詢的說明。

建立表示式樹

建立表示式樹的最簡單方法是使用 lambda 表示式。這些表示式看起來與普通的 C#方法幾乎相同。重要的是要意識到這是編譯器的魔力。首次建立 lambda 表示式時,編譯器會檢查你為其分配的內容。如果它是 Delegate 型別(包括 ActionFunc),編譯器會將 lambda 表示式轉換為委託。如果它是 LambdaExpression(或 Expression<Action<T>>Expression<Func<T>>,它們是強型別 LambdaExpression 的),編譯器會將其轉換為 LambdaExpression。這就是魔術的結果。在幕後,編譯器使用表示式樹 API 將你的 lambda 表示式轉換為 LambdaExpression

Lambda 表示式無法建立每種型別的表示式樹。在這些情況下,你可以手動使用 Expressions API 來建立所需的樹。在 Understanding the expressions API 示例中,我們使用 API​​建立 CalculateTotalSalesTax 表示式。

注意:名稱在這裡有點令人困惑。甲 lambda 表示式 (兩個字,小寫)指的程式碼與 => 指示符的塊。它代表 C#中的匿名方法,並轉換為 DelegateExpression。甲 LambdaExpression (一個字,PascalCase)指的是表達 API 代表可以執行的方法中的節點型別。

表示式樹和 LINQ

表示式樹的最常見用途之一是使用 LINQ 和資料庫查詢。LINQ 將表示式樹與查詢提供程式配對,以將指令應用於目標遠端查詢。例如,LINQ to Entity Framework 查詢提供程式將表示式樹轉換為 SQL,該 SQL 直接針對資料庫執行。

將所有部分組合在一起,你可以看到 LINQ 背後的真正力量。

  1. 使用 lambda 表示式編寫查詢:products.Where(x => x.Cost > 5)
  2. 編譯器將該表示式轉換為表示式樹,其中包含“檢查引數的 Cost 屬性是否大於 5”的指令。
  3. 查詢提供程式解析表示式樹並生成有效的 SQL 查詢 SELECT * FROM products WHERE Cost > 5
  4. ORM 將所有結果投射到 POCO 中,然後返回一個物件列表

筆記

  • 表示式樹是不可變的。如果要更改表示式樹,則需要建立一個新表示式樹,將現有樹複製到新表達樹中(遍歷表示式樹,你可以使用 ExpressionVisitor)並進行所需的更改。