次の方法で共有


式ツリーを変換する

この記事では、式ツリーの変更されたコピーを作成するときに、式ツリー内の各ノードにアクセスする方法について説明します。 式ツリーを翻訳してアルゴリズムを理解し、別の環境に変換できるようにします。 作成されたアルゴリズムを変更します。 ログ記録を追加したり、メソッド呼び出しをインターセプトして追跡したり、その他の目的で実行したりできます。

式ツリーを変換するためにビルドするコードは、ツリー内のすべてのノードにアクセスするために既に見てきたものの拡張機能です。 式ツリーを翻訳するときは、すべてのノードにアクセスし、そのノードを参照しながら新しいツリーを構築します。 新しいツリーには、元のノードへの参照、またはツリーに配置した新しいノードが含まれている場合があります。

式ツリーにアクセスし、置換ノードをいくつか含む新しいツリーを作成してみましょう。 この例では、任意の定数を 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);

新しいツリーの構築は、既存のツリー内のノードにアクセスし、新しいノードを作成してツリーに挿入する組み合わせです。 前の例は、式ツリーが不変であることの重要性を示しています。 前のコードで作成された新しいツリーには、新しく作成されたノードと、既存のツリーのノードが混在していることに注意してください。 既存のツリー内のノードは変更できないため、両方のツリーでノードを使用できます。 ノードを再利用すると、メモリ効率が大幅に向上します。 同じノードは、ツリー全体で、または複数の式ツリーで使用できます。 ノードは変更できないため、必要なときにいつでも同じノードを再利用できます。

加算を走査して実行する

追加ノードのツリーを歩いて結果を計算する 2 番目のビジターを構築して、新しいツリーを確認しましょう。 これまでに見てきた訪問者に対していくつかの変更を加えます。 この新しいバージョンでは、ビジターは、この時点までの加算操作の部分的な合計を返します。 定数式の場合は、単に定数式の値です。 加算式の場合、ツリーの走査が完了すると、結果は左右のオペランドの合計になります。

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 ディレクティブを追加します。 Program.cs ファイル内の Main メソッドにコードを追加して式ツリーを作成し、それを変更するメソッドに渡します。

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 の このシリーズ を参照してください。 式ツリーで見つかる可能性のあるコードを翻訳する方法について詳しく説明します。

式ツリーの真の力を見てきました。 一連のコードを調べ、そのコードに変更を加えて、変更されたバージョンを実行します。 式ツリーは不変であるため、既存のツリーのコンポーネントを使用して新しいツリーを作成します。 ノードを再利用すると、変更された式ツリーを作成するために必要なメモリの量が最小限に抑えられます。