共用方式為


執行表示式樹

表達式樹狀結構是代表某些程式代碼的數據結構。 它不是編譯好的,也不是可執行的代碼。 如果您要執行表示式樹狀結構所代表的 .NET 程式代碼,您必須將它轉換成可執行的 IL 指令。 執行表達式樹狀結構可能會傳回值,或者它可能只執行像是呼叫 方法的動作。

只能執行代表 Lambda 表達式的表達式樹。 代表 Lambda 表示式的運算式樹狀結構類型為 LambdaExpressionExpression<TDelegate>。 若要執行這些表達式樹狀架構,請呼叫 Compile 方法來建立可執行的委派,然後叫用委派。

備註

如果委派的類型未知,也就是說,Lambda 表達式的類型 LambdaExpression 不是 Expression<TDelegate>,應該在委派上呼叫 DynamicInvoke 方法,而不是直接叫用它。

如果表達式樹狀結構不代表 Lambda 運算式,您可以藉由呼叫 Lambda<TDelegate>(Expression, IEnumerable<ParameterExpression>) 方法,建立具有原始運算式樹狀結構作為其主體的新 Lambda 運算式。 然後,您可以執行 Lambda 表達式,如本節稍早所述。

將 Lambda 運算式轉換為函式

您可以將任何 LambdaExpression 或衍生自 LambdaExpression 的任何類型轉換成可執行的 IL。 其他表達式類型無法直接轉換成程序代碼。 實際上,這項限制幾乎沒有作用。 Lambda 運算式是您想要透過轉換成可執行中繼語言 (IL) 來執行的唯一表達式類型。 (想想直接執行 System.Linq.Expressions.ConstantExpression的意義。這是否意味著任何有用的東西?任何為 System.Linq.Expressions.LambdaExpression的表達式樹狀結構,或衍生自 LambdaExpression 的類型都可以轉換成 IL。 表達式類型 System.Linq.Expressions.Expression<TDelegate> 是 .NET Core 連結庫中唯一的具體範例。 它用來表示對應至任何委派類型的表達式。 因為此類型會對應至委託類型,因此 .NET 可以檢查表達式,並為符合 Lambda 運算式簽名的適當委託產生 IL。 委派類型是以表達式類型為基礎。 如果您想要以強類型的方式使用委派物件,則必須知道傳回類型和參數列表。 方法會 LambdaExpression.Compile() 傳回型別 Delegate 。 您必須將它轉換成正確的委派類型,讓任何編譯時期工具檢查自變數清單或傳回類型。

在大部分情況下,表達式和其對應委派之間存在一個簡單的映射。 例如,由 Expression<Func<int>> 所表示的表達式樹狀結構會轉換成型別為 Func<int> 的委派。 對於具有任何傳回類型和自變數清單的 Lambda 表達式,有委派類型是該 Lambda 運算式所表示之可執行程式代碼的目標類型。

類型 System.Linq.Expressions.LambdaExpression 包含 LambdaExpression.CompileLambdaExpression.CompileToMethod 成員,您將用來將表達式樹狀結構轉換成可執行的程式代碼。 方法 Compile 會建立委派。 此方法會使用代表表達式樹狀結構的編譯輸出的 IL 來更新CompileToMethod物件。

這很重要

CompileToMethod 僅適用於 .NET Framework,不適用於 .NET Core 或 .NET 5 和更新版本。

或者,您也可以提供 System.Runtime.CompilerServices.DebugInfoGenerator ,以接收所產生委派對象的符號偵錯資訊。 DebugInfoGenerator提供所產生委派的完整偵錯資訊。

您可以使用以下程式碼將表示式轉換為委派:

Expression<Func<int>> add = () => 1 + 2;
var func = add.Compile(); // Create Delegate
var answer = func(); // Invoke Delegate
Console.WriteLine(answer);

下列程式代碼範例示範編譯和執行表達式樹狀結構時所使用的具體類型。

Expression<Func<int, bool>> expr = num => num < 5;

// Compiling the expression tree into a delegate.
Func<int, bool> result = expr.Compile();

// Invoking the delegate and writing the result to the console.
Console.WriteLine(result(4));

// Prints True.

// You can also use simplified syntax
// to compile and run an expression tree.
// The following line can replace two previous statements.
Console.WriteLine(expr.Compile()(4));

// Also prints True.

下列程式代碼範例示範如何執行表達式樹狀結構,此樹狀結構表示藉由建立 Lambda 運算式和執行它,將數位提升至乘冪。 顯示出代表數值提高到次方的結果。

// The expression tree to execute.
BinaryExpression be = Expression.Power(Expression.Constant(2d), Expression.Constant(3d));

// Create a lambda expression.
Expression<Func<double>> le = Expression.Lambda<Func<double>>(be);

// Compile the lambda expression.
Func<double> compiledExpression = le.Compile();

// Execute the lambda expression.
double result = compiledExpression();

// Display the result.
Console.WriteLine(result);

// This code produces the following output:
// 8

執行和生命週期

要執行程式碼,請叫用您在呼叫 LambdaExpression.Compile() 時建立的委派。 前述程式碼 add.Compile() 會傳回委派。 您可以藉由呼叫 func() 來叫用該委派,這會執行程式碼。

該委派代表表達式樹中的程式碼。 您可以保留該委派的句柄,並在稍後加以叫用。 每次您想要執行它所代表的程式代碼時,您不需要編譯表達式樹狀結構。 (請記住,表達式樹狀結構是不可變的,而編譯相同的運算式樹狀結構稍後會建立執行相同程序代碼的委派。

謹慎

請勿建立任何更複雜的快取機制,以避免不必要的編譯呼叫來提升效能。 比較兩個任意表達式樹狀結構,以判斷它們是否代表相同的演算法是耗時的作業。 您透過避免任何額外呼叫 LambdaExpression.Compile() 節省的計算時間,可能會被用於執行程式碼,以判斷兩個不同的運算式樹狀結構是否生成相同的可執行代碼的時間所抵消。

警示

將 Lambda 表達式編譯為委派並呼叫該委派,是您可以使用運算式樹執行的簡單操作之一。 不過,即使使用這個簡單的操作,您也必須注意一些注意事項。

Lambda 運算式會在表達式中所參考的任何局部變數上建立閉包。 您必須確保所有作為委託一部分的變數,不論是在調用Compile的位置還是在執行生成的委託時,都是可用的。 編譯程式可確保變數在範圍內。 不過,如果您的表達式存取了實作IDisposable的變數,則您的程式碼可能會在表達式樹仍保留時處置物件。

例如,此程式代碼正常運作,因為 int 沒有實作 IDisposable

private static Func<int, int> CreateBoundFunc()
{
    var constant = 5; // constant is captured by the expression tree
    Expression<Func<int, int>> expression = (b) => constant + b;
    var rVal = expression.Compile();
    return rVal;
}

委派已擷取局部變數 constant的參考。 該變數會在稍後執行 所 CreateBoundFunc 傳回的函式時存取。

不過,請考慮實作 System.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;
    }
}

如果您在運算式中使用它,如下列程式碼所示,當您執行System.ObjectDisposedException屬性所參考的程式碼時,您會得到Resource.Argument

private static Func<int, int> CreateBoundResource()
{
    using (var constant = new Resource()) // constant is captured by the expression tree
    {
        Expression<Func<int, int>> expression = (b) => constant.Argument + b;
        var rVal = expression.Compile();
        return rVal;
    }
}

從這個方法傳回的委派已經關閉物件 constant ,該對象已經處置。 (它已被處置,因為它是在 using 語句中宣告的。)

現在,當您執行從這個方法傳回的委派時,會在 ObjectDisposedException 執行點擲回 。

運行時錯誤代表編譯時結構確實聽起來很奇怪,但當您使用表示式樹時,這就是您所進入的世界。

這個問題有許多變化,因此很難提供一般的指引來避免它。 在定義表達式時,請小心存取局部變數,並在建立透過公用 API 傳回的表達式樹狀結構時,小心存取目前物件中的狀態(以 this表示)。

表達式中的程式代碼可能會參考其他元件中的方法或屬性。 在定義表達式、編譯表達式及叫用產生的委派時,必須能存取該元件。 如果您在某些情況下遇到ReferencedAssemblyNotFoundException,它通常不存在。

總結

代表 Lambda 表達式的運算式樹可以編譯,從而建立您可以執行的委派。 表達式樹狀架構提供一個機制來執行表達式樹狀結構所表示的程序代碼。

表達式樹確實代表所執行的程式碼,適用於您所建立的任何構造。 只要您編譯和執行程式代碼的環境符合您建立表達式的環境,一切都會如預期般運作。 當這種情況未發生時,錯誤是可預測的,而且會在您使用表達式樹對任何程式碼的第一次測試中檢測到。