共用方式為


轉譯表達式樹

在本文中,您會瞭解如何瀏覽表達式樹狀結構中的每個節點,同時建置該表達式樹狀結構的修改複本。 您轉譯表達式樹,以理解演算法,從而將其轉換至其他環境。 您可以變更已建立的演算法。 您可以新增記錄、攔截方法呼叫並追蹤它們,或用於其他目的。

您建置以轉譯表達式樹狀結構的程式代碼是您已看到流覽樹狀結構中所有節點的延伸模組。 當您轉譯表達式樹狀結構時,您會流覽所有節點,並在瀏覽這些節點時建置新的樹狀結構。 新的樹狀結構可能包含原始節點的參考,或您在樹狀結構中放置的新節點。

讓我們瀏覽表達式樹,並建立一個包含一些替換節點的新樹。 在此範例中,讓我們將任何常數取代為大於10倍的常數。 否則,您會讓表達式樹狀結構保持不變。 您不需要讀取常數的值,並以新的常數取代它,而是將常數節點取代為執行乘法的新節點。

在這裡,一旦找到常數節點,您就會建立新的乘法節點,其子系為原始常數和常數 10

private static Expression ReplaceNodes(Expression original)
{
    if (original.NodeType == ExpressionType.Constant)
    {
        return Expression.Multiply(original, Expression.Constant(10));
    }
    else if (original.NodeType == ExpressionType.Add)
    {
        var binaryExpression = (BinaryExpression)original;
        return Expression.Add(
            ReplaceNodes(binaryExpression.Left),
            ReplaceNodes(binaryExpression.Right));
    }
    return original;
}

將原始節點替換為替代節點,以建立新的樹狀結構。 您可以編譯並執行已取代的樹狀結構來驗證變更。

var one = Expression.Constant(1, typeof(int));
var two = Expression.Constant(2, typeof(int));
var addition = Expression.Add(one, two);
var sum = ReplaceNodes(addition);
var executableFunc = Expression.Lambda(sum);

var func = (Func<int>)executableFunc.Compile();
var answer = func();
Console.WriteLine(answer);

建置新的樹狀結構是流覽現有樹狀結構中的節點,以及建立新節點並將其插入樹狀結構的組合。 上一個範例顯示表達式樹狀結構不可變的重要性。 請注意,在上述程序代碼中建立的新樹狀結構包含新建立的節點和現有樹狀結構中的節點混合。 這兩個樹狀結構都可以使用節點,因為無法修改現有樹狀結構中的節點。 重複使用節點會產生顯著的記憶體效率。 相同的節點可以在整個樹狀結構或多個表達式樹狀結構中使用。 因為節點無法修改,所以只要需要,就可以重複使用相同的節點。

遍歷並執行加法

讓我們藉由建置第二個訪問者來驗證新的樹,遍歷加法節點的樹並計算結果。 對到目前為止見過的訪客進行一些修改。 在這個新版本中,訪客會傳回加法作業的部分總和,到目前為止。 對於常數表達式,它只是常數表達式的值。 對於加法表達式,一旦遍歷這些樹狀結構,結果是左運算元和右運算元的總和。

var one = Expression.Constant(1, typeof(int));
var two = Expression.Constant(2, typeof(int));
var three = Expression.Constant(3, typeof(int));
var four = Expression.Constant(4, typeof(int));
var addition = Expression.Add(one, two);
var add2 = Expression.Add(three, four);
var sum = Expression.Add(addition, add2);

// Declare the delegate, so you can call it
// from itself recursively:
Func<Expression, int> aggregate = null!;
// Aggregate, return constants, or the sum of the left and right operand.
// Major simplification: Assume every binary expression is an addition.
aggregate = (exp) =>
    exp.NodeType == ExpressionType.Constant ?
    (int)((ConstantExpression)exp).Value :
    aggregate(((BinaryExpression)exp).Left) + aggregate(((BinaryExpression)exp).Right);

var theSum = aggregate(sum);
Console.WriteLine(theSum);

這裡有相當多的程序代碼,但概念是可接近的。 此程式碼會使用深度優先搜尋來訪問子節點。 當它遇到常數節點時,訪客會傳回常數的值。 參觀者訪問這兩個子系之後,這些子系已計算該子樹的計算總和。 加法節點現在可以計算其總和。 一旦瀏覽表達式樹狀結構中的所有節點,就會計算總和。 您可以在調試程式中執行範例並追蹤程序的執行。

讓我們更輕鬆地追蹤節點的分析過程,以及遍歷樹形結構來計算總和的過程。 以下是 Aggregate 方法的更新版本,其中包含相當多的追蹤資訊:

private static int Aggregate(Expression exp)
{
    if (exp.NodeType == ExpressionType.Constant)
    {
        var constantExp = (ConstantExpression)exp;
        Console.Error.WriteLine($"Found Constant: {constantExp.Value}");
        if (constantExp.Value is int value)
        {
            return value;
        }
        else
        {
            return 0;
        }
    }
    else if (exp.NodeType == ExpressionType.Add)
    {
        var addExp = (BinaryExpression)exp;
        Console.Error.WriteLine("Found Addition Expression");
        Console.Error.WriteLine("Computing Left node");
        var leftOperand = Aggregate(addExp.Left);
        Console.Error.WriteLine($"Left is: {leftOperand}");
        Console.Error.WriteLine("Computing Right node");
        var rightOperand = Aggregate(addExp.Right);
        Console.Error.WriteLine($"Right is: {rightOperand}");
        var sum = leftOperand + rightOperand;
        Console.Error.WriteLine($"Computed sum: {sum}");
        return sum;
    }
    else throw new NotSupportedException("Haven't written this yet");
}

sum 表示式上執行它會產生下列輸出:

10
Found Addition Expression
Computing Left node
Found Addition Expression
Computing Left node
Found Constant: 1
Left is: 1
Computing Right node
Found Constant: 2
Right is: 2
Computed sum: 3
Left is: 3
Computing Right node
Found Addition Expression
Computing Left node
Found Constant: 3
Left is: 3
Computing Right node
Found Constant: 4
Right is: 4
Computed sum: 7
Right is: 7
Computed sum: 10
10

追蹤輸出,並遵循前面的代碼。 您應該能夠了解程式碼如何遍歷每個節點,並在遍歷樹狀結構時計算出總和。

現在,讓我們看看不同的執行過程,由sum1給出的表達式:

Expression<Func<int>> sum1 = () => 1 + (2 + (3 + 4));

以下是分析此表示式的結果:

Found Addition Expression
Computing Left node
Found Constant: 1
Left is: 1
Computing Right node
Found Addition Expression
Computing Left node
Found Constant: 2
Left is: 2
Computing Right node
Found Addition Expression
Computing Left node
Found Constant: 3
Left is: 3
Computing Right node
Found Constant: 4
Right is: 4
Computed sum: 7
Right is: 7
Computed sum: 9
Right is: 9
Computed sum: 10
10

雖然最終答案相同,但樹的遍歷方式有所不同。 節點會以不同的順序移動,因為樹狀結構是先以不同的作業所建構。

建立修改過的複本

建立新的 主控台應用程式 專案。 將 using 指示詞新增至檔案,該檔案屬於System.Linq.Expressions命名空間。 將類別 AndAlsoModifier 新增至您的專案。

public class AndAlsoModifier : ExpressionVisitor
{
    public Expression Modify(Expression expression)
    {
        return Visit(expression);
    }

    protected override Expression VisitBinary(BinaryExpression b)
    {
        if (b.NodeType == ExpressionType.AndAlso)
        {
            Expression left = this.Visit(b.Left);
            Expression right = this.Visit(b.Right);

            // Make this binary expression an OrElse operation instead of an AndAlso operation.
            return Expression.MakeBinary(ExpressionType.OrElse, left, right, b.IsLiftedToNull, b.Method);
        }

        return base.VisitBinary(b);
    }
}

這個類別繼承ExpressionVisitor類別,並專門用於修改代表條件AND運算的表達式。 它會將這些作業從條件 AND 式變更為條件 OR式 。 類別覆寫基底類型的VisitBinary方法,因為條件AND表達式是用二進位表達式表示。 在方法中 VisitBinary ,如果傳遞給它的表達式代表條件 AND 運算,程式代碼會建構新的運算式,其中包含條件 OR 運算符,而不是條件 AND 運算符。 如果傳遞至 VisitBinary 的表達式不代表條件 AND 式作業,則方法會延遲至基類實作。 基類方法會建構類似傳入之表達式樹狀架構的節點,但節點的子樹狀結構已由訪客以遞歸方式產生的表達式樹狀結構取代。

using 指示詞新增至檔案,該檔案屬於System.Linq.Expressions命名空間。 將程式代碼新增至 Main Program.cs 檔案中的 方法,以建立表達式樹狀結構,並將它傳遞給修改它的方法。

Expression<Func<string, bool>> expr = name => name.Length > 10 && name.StartsWith("G");
Console.WriteLine(expr);

AndAlsoModifier treeModifier = new AndAlsoModifier();
Expression modifiedExpr = treeModifier.Modify((Expression)expr);

Console.WriteLine(modifiedExpr);

/*  This code produces the following output:

    name => ((name.Length > 10) && name.StartsWith("G"))
    name => ((name.Length > 10) || name.StartsWith("G"))
*/

程序代碼會建立包含條件 AND 式作業的表達式。 然後,它會建立AndAlsoModifier類別的實例,並將表達式傳遞至此類別的Modify方法。 系統會輸出原始和修改過的運算式樹狀結構,以顯示變更。 編譯並執行應用程式。

瞭解更多資訊

此範例顯示您建置的程式碼,用於遍歷並解釋表達式樹所代表的演算法的一小部分。 如需了解建置能將表達式樹轉譯成另一種語言的一般用途函式庫的資訊,請閱讀 Matt Warren 所撰寫的這個系列。 它會詳細說明如何轉譯您在表達式樹狀結構中找到的任何程序代碼。

您現在已看到表達式樹狀架構的真正威力。 您會檢查一組程式代碼、對該程式代碼進行任何變更,並執行變更的版本。 因為表達式樹狀結構是不可變的,所以您可以使用現有樹狀架構的元件來建立新的樹狀結構。 重複使用節點可將建立修改表達式樹狀結構所需的記憶體數量降到最低。