C#3.0新增功能10 表達式樹 04 執行表達式

  • 2019 年 10 月 4 日
  • 筆記

表達式樹 是表示一些程式碼的數據結構。 它不是已編譯且可執行的程式碼。 如果想要執行由表達式樹表示的 .NET 程式碼,則必須將其轉換為可執行的 IL 指令。

Lambda 表達式到函數

可以將任何 LambdaExpression 或派生自 LambdaExpression 的任何類型轉換為可執行的 IL。 其他表達式類型不能直接轉換為程式碼。 此限制在實踐中影響不大。 Lambda 表達式是你可通過轉換為可執行的中間語言 (IL) 來執行的唯一表達式類型。 (思考直接執行 ConstantExpression 意味著什麼。 這是否意味著任何用處?)LambdaExpression 或派生自 LambdaExpression 的類型的任何表達式樹均可轉換為 IL。 表達式類型 Expression<TDelegate> 是 .NET Core 庫中的唯一具體示例。 它用於表示映射到任何委託類型的表達式。 由於此類型映射到一個委託類型,因此 .NET 可以檢查表達式,並為匹配 lambda 表達式簽名的適當委託生成 IL。

在大多數情況下,這將在表達式和其對應的委託之間創建簡單映射。 例如,由 Expression<Func<int>> 表示的表達式樹將被轉換為 Func<int> 類型的委託。 對於具有任何返回類型和參數列表的 Lambda 表達式,存在這樣的委託類型:該類型是由該 Lambda 表達式表示的可執行程式碼的目標類型。

LambdaExpression 類型包含用於將表達式樹轉換為可執行程式碼的 CompileCompileToMethod成員。 Compile 方法創建委託。 CompileToMethod 方法通過表示表達式樹的已編譯輸出的 IL 更新 MethodBuilder 對象。 請注意,CompileToMethod 僅在完整的桌面框架中可用,不能用於 .NET Core。

還可以選擇性地提供 DebugInfoGenerator,它將接收生成的委託對象的符號調試資訊。 這讓你可以將表達式樹轉換為委託對象,並擁有生成的委託的完整調試資訊。

使用下面的程式碼將表達式轉換為委託:

Expression<Func<int>> add = () => 1 + 2;  var func = add.Compile(); // 生產委託  var answer = func();      // 執行委託  Console.WriteLine(answer);

請注意,該委託類型基於表達式類型。 如果想要以強類型的方式使用委託對象,則必須知道返回類型和參數列表。 LambdaExpression.Compile() 方法返回 Delegate 類型。 必須將其轉換為正確的委託類型,以便使任何編譯時工具檢查參數列表或返回類型。

執行和生存期

通過調用在調用 LambdaExpression.Compile() 時創建的委託來執行程式碼。 可以在上面進行查看,其中 add.Compile() 返回了一個委託。 通過調用 func() 調用該委託將執行程式碼。

該委託表示表達式樹中的程式碼。 可以保留該委託的句柄並在稍後調用它。 不需要在每次想要執行表達式樹所表示的程式碼時編譯表達式樹。 (請記住,表達式樹是不可變的,且在之後編譯同一表達式樹將創建執行相同程式碼的委託。)

在此提醒你不要通過避免不必要的編譯調用嘗試創建用於提高性能的任何更複雜的快取機制。 比較兩個任意的表達式樹,以確定如果它們表示相同的演算法,是否也會花費很長的時間來執行。 你可能會發現,通過避免對 LambdaExpression.Compile() 的任何額外調用所節省的計算時間將多於執行程式碼(該程式碼確定可導致相同可執行程式碼的兩個不同表達式樹)所花費的時間。

注意事項

將 lambda 表達式編譯為委託並調用該委託是可對表達式樹執行的最簡單的操作之一。 但是,即使是執行這個簡單的操作,也存在一些必須注意的事項。

Lambda 表達式將對表達式中引用的任何局部變數創建閉包。 必須保證作為委託的一部分的任何變數在調用 Compile 的位置處和執行結果委託時可用。

一般情況下,編譯器會確保這一點。 但是,如果表達式訪問實現 IDisposable 的變數,則程式碼可能在表達式樹仍保留有對象時釋放該對象。

例如,此程式碼工作正常,因為 int 不實現 IDisposable

private static Func<int, int> CreateBoundFunc()  {      var constant = 5; // 常量由表達式樹捕獲      Expression<Func<int, int>> expression = (b) => constant + b;      var rVal = expression.Compile();      return rVal;  }

委託已捕獲對局部變數 constant 的引用。 在稍後執行 CreateBoundFunc 返回的函數之後,可隨時訪問該變數。

但是,請考慮實現 IDisposable 的此(人為設計的)類:

public class Resource : IDisposable  {      private bool isDisposed = false;      public int Argument      {          get          {              if (!isDisposed)                  return 5;              else throw new ObjectDisposedException("Resource");          }      }        public void Dispose()      {          isDisposed = true;      }  }

如果將其用於如下所示的表達式中,則在執行 Resource.Argument 屬性引用的程式碼時將出現 ObjectDisposedException

private static Func<int, int> CreateBoundResource()  {      using (var constant = new Resource()) // 常量由表達式樹捕獲      {          Expression<Func<int, int>> expression = (b) => constant.Argument + b;          var rVal = expression.Compile();          return rVal;      }  }

從此方法返回的委託已對釋放了的 constant 對象閉包。 (它已被釋放,因為它已在 using 語句中進行聲明。)

現在,在執行從此方法返回的委託時,將在執行時引發 ObjectDisposedException

出現表示編譯時構造的運行時錯誤確實很奇怪,但這是使用表達式樹時的正常現象。

此問題存在大量的排列,因此很難提供用於避免此問題的一般性指導。 定義表達式時,請謹慎訪問局部變數,且在創建可由公共 API 返回的表達式樹時,謹慎訪問當前對象(由 this 表示)中的狀態。

表達式中的程式碼可能引用其他程式集中的方法或屬性。 對表達式進行定義、編譯或在調用結果委託時,該程式集必須可訪問。 在它不存在的情況下,將遇到 ReferencedAssemblyNotFoundException

總結

可以編譯表示 lambda 表達式的表達式樹,以創建可執行的委託。 這提供了一種機制,用於執行表達式樹所表示的程式碼。

表達式樹表示會為創建的任意給定構造執行的程式碼。 只要編譯和執行程式碼的環境匹配創建表達式的環境,則一切將按預期進行。 如果未按預期進行,那麼錯誤也是很容易預知的,並且將在使用表達式樹的任何程式碼的第一個測試中捕獲這些錯誤。