建置運算式樹狀架構

C# 編譯器已建立您目前為止看到的所有運算式樹狀架構。 您建立了 Lambda 運算式,指派給類型為 Expression<Func<T>> 的變數或類似類型。 在許多情況下,您會在執行階段的記憶體中建立一個運算式。

運算式樹狀結構不可變。 所謂不可變,表示樹狀結構必須從分葉向上組建到根。 您要用來組建運算式樹狀架構的 API 反映這項事實︰組建節點要使用的方法會採用其所有子系作為引數。 讓我們逐步解說幾個範例,向您示範技巧。

建立節點

在下列各節中,您會先從前面用過的加法運算式開始︰

Expression<Func<int>> sum = () => 1 + 2;

若要建構該運算式樹狀架構,您必須建構分葉節點。 分葉節點是常數。 使用 Constant 方法以建立節點:

var one = Expression.Constant(1, typeof(int));
var two = Expression.Constant(2, typeof(int));

接下來,建置加法運算式︰

var addition = Expression.Add(one, two);

建置加法運算式之後,您需要建立 Lambda 運算式︰

var lambda = Expression.Lambda(addition);

此 Lambda 運算式不包含任何引數。 稍後在本節中,您會看到引數如何對應至參數,並建置更複雜的運算式。

針對與這個類似運算式,您可以將所有呼叫結合成單一陳述式︰

var lambda2 = Expression.Lambda(
    Expression.Add(
        Expression.Constant(1, typeof(int)),
        Expression.Constant(2, typeof(int))
    )
);

建置樹狀結構

上一節顯示在記憶體中建置運算式樹狀結構的基本概念。 更複雜的樹狀結構通常表示在樹狀目錄中有更多的節點類型和更多的節點。 讓我們再執行一個範例,並多顯示兩個當您建立運算式樹狀架構時,通常會組建的節點類型︰引數節點和方法呼叫節點。 我們要組建運算式樹狀架構來建立此運算式︰

Expression<Func<double, double, double>> distanceCalc =
    (x, y) => Math.Sqrt(x * x + y * y);

您會從建立 xy 的參數運算式開始:

var xParameter = Expression.Parameter(typeof(double), "x");
var yParameter = Expression.Parameter(typeof(double), "y");

建立乘法和加法運算式要遵循您已見過的模式︰

var xSquared = Expression.Multiply(xParameter, xParameter);
var ySquared = Expression.Multiply(yParameter, yParameter);
var sum = Expression.Add(xSquared, ySquared);

接下來,您需要為 Math.Sqrt 呼叫建立方法呼叫運算式。

var sqrtMethod = typeof(Math).GetMethod("Sqrt", new[] { typeof(double) }) ?? throw new InvalidOperationException("Math.Sqrt not found!");
var distance = Expression.Call(sqrtMethod, sum);

如果找不到方法,則 GetMethod 呼叫可能會傳回 null。 很可能是因為您拼錯了方法名稱。 否則,這可能表示未載入必要的組件。 最後,將方法呼叫放入 Lambda 運算式中,確定定義 Lambda 運算式的引數︰

var distanceLambda = Expression.Lambda(
    distance,
    xParameter,
    yParameter);

在這個更複雜的範例中,您會多看到幾個建立運算式樹狀架構經常需要的技巧。

首先,您需要先建立代表參數或區域變數的物件,再使用它們。 建立這些物件之後,就可以在您的運算式樹狀架構中隨時使用它們。

其次,您需要使用反映 API 的子集來建立 System.Reflection.MethodInfo 物件,以便可以建立運算式樹狀架構來存取該方法。 您必須將自己限制在 .NET Core 平台上可用的反映 API 子集。 同樣地,這些技巧會延伸到其他的運算式樹狀架構。

深入建置程式碼

您不會受限於使用這些 API 所可以組建的項目。 不過,想要組建的運算式樹狀架構越複雜,程式碼就越難管理及閱讀。

我們要組建相當於這個程式碼的運算式樹狀架構︰

Func<int, int> factorialFunc = (n) =>
{
    var res = 1;
    while (n > 1)
    {
        res = res * n;
        n--;
    }
    return res;
};

上述程式碼並未建置運算式樹狀結構,而只是委派。 使用 Expression 類別就無法建立陳述式 Lambda。 以下是組建相同功能需要的程式碼。 沒有 API 可用來建置 while 迴圈,您需要改組建包含條件式測試的迴圈,以及脫離迴圈的標籤目標。

var nArgument = Expression.Parameter(typeof(int), "n");
var result = Expression.Variable(typeof(int), "result");

// Creating a label that represents the return value
LabelTarget label = Expression.Label(typeof(int));

var initializeResult = Expression.Assign(result, Expression.Constant(1));

// This is the inner block that performs the multiplication,
// and decrements the value of 'n'
var block = Expression.Block(
    Expression.Assign(result,
        Expression.Multiply(result, nArgument)),
    Expression.PostDecrementAssign(nArgument)
);

// Creating a method body.
BlockExpression body = Expression.Block(
    new[] { result },
    initializeResult,
    Expression.Loop(
        Expression.IfThenElse(
            Expression.GreaterThan(nArgument, Expression.Constant(1)),
            block,
            Expression.Break(label, result)
        ),
        label
    )
);

組建階乘函式運算式樹狀結構的程式碼相當長,也更複雜,滿是標籤和 break 陳述式及其他項目,是您日常編碼工作想要避免的坑。

本節中您撰寫了程式碼,可瀏覽此運算式樹狀結構中的每個節點,並寫出此範例所建立之節點的相關資訊。 您可以在 dotnet/docs GitHub 存放庫檢視或下載範例程式碼。 建置並執行範例來親自試驗。

將程式碼建構對應至運算式

下列程式碼範例示範使用 API 建立代表 Lambda 運算式 num => num < 5 的運算式樹狀架構。

// Manually build the expression tree for
// the lambda expression num => num < 5.
ParameterExpression numParam = Expression.Parameter(typeof(int), "num");
ConstantExpression five = Expression.Constant(5, typeof(int));
BinaryExpression numLessThanFive = Expression.LessThan(numParam, five);
Expression<Func<int, bool>> lambda1 =
    Expression.Lambda<Func<int, bool>>(
        numLessThanFive,
        new ParameterExpression[] { numParam });

運算式樹狀架構 API 也支援指派以及控制流程運算式 (例如迴圈、條件式區塊和 try-catch 區塊)。 使用 API 建立的運算式樹狀架構,可以比使用 C# 編譯器從 Lambda 運算式建立的運算式樹狀架構更加複雜。 下列範例示範如何建立可計算數字階乘的運算式樹狀架構。

// Creating a parameter expression.
ParameterExpression value = Expression.Parameter(typeof(int), "value");

// Creating an expression to hold a local variable.
ParameterExpression result = Expression.Parameter(typeof(int), "result");

// Creating a label to jump to from a loop.
LabelTarget label = Expression.Label(typeof(int));

// Creating a method body.
BlockExpression block = Expression.Block(
    // Adding a local variable.
    new[] { result },
    // Assigning a constant to a local variable: result = 1
    Expression.Assign(result, Expression.Constant(1)),
        // Adding a loop.
        Expression.Loop(
           // Adding a conditional block into the loop.
           Expression.IfThenElse(
               // Condition: value > 1
               Expression.GreaterThan(value, Expression.Constant(1)),
               // If true: result *= value --
               Expression.MultiplyAssign(result,
                   Expression.PostDecrementAssign(value)),
               // If false, exit the loop and go to the label.
               Expression.Break(label, result)
           ),
       // Label to jump to.
       label
    )
);

// Compile and execute an expression tree.
int factorial = Expression.Lambda<Func<int, int>>(block, value).Compile()(5);

Console.WriteLine(factorial);
// Prints 120.

如需詳細資訊,請參閱在 Visual Studio 2010 中使用運算式樹狀架構產生動態方法 (英文),這也適用於更新版本的 Visual Studio。