式ツリーを作成する

これまでに見てきたすべての式ツリーは、C# コンパイラによって作成されました。 開発者は、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);

加算式を作成したら、ラムダ式を作成します。

var lambda = Expression.Lambda(addition);

このラムダ式には、引数は含まれません。 このセクションで後ほど、引数をパラメーターにマップし、より複雑な式を作成する方法について説明します。

このような式の場合、すべての呼び出しを 1 つのステートメントにまとめることができます。

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

ツリーを作成する

前のセクションでは、メモリ内での式ツリーの作成に関する基本を示しました。 一般的に、複雑なツリーの場合はノードの種類が多く、ツリー内のノード数も多くなります。 もう 1 つの例を使って、式ツリーを作成するときに一般的に構築する 2 つのノードである引数ノードとメソッド呼び出しノードについて説明します。 式ツリーを構築して次の式を作成しましょう。

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 を返す可能性があります。 最も可能性が高いのは、メソッド名のスペルが間違っていることです。 それ以外では、必要なアセンブリが読み込まれていない可能性があります。 最後に、メソッド呼び出しをラムダ式に組み込み、忘れずにラムダ式の引数を定義します。

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

このやや複雑な例では、式ツリーの作成で必要になることがよくある手法がさらに 2 つ示されています。

まず、パラメーターまたはローカル変数を表すオブジェクトは、使用前に作成しておく必要があります。 オブジェクトの作成後は、式ツリーのどこでも必要な場所で使用できます。

次に、System.Reflection.MethodInfo オブジェクトを作成するには、リフレクション API の一部を使用する必要があります。これは、そのメソッドにアクセスする式ツリーを作成できるようにするためです。 使用するリフレクション API は、.NET Core プラットフォームで使用できるものに限定する必要があります。 繰り返しになりますが、こうした手法は他の式ツリーにも応用されます。

コードの作成の詳細

このような API を使用して構築できるものに制限はありませんが、 構築する式ツリーが複雑になるほど、コードの管理と読み取りが困難になります。

このコードと同じ式ツリーを構築してみましょう。

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

上のコードでは、式ツリーを作成せず、デリゲートのみでした。 Expression クラスを使用して、ステートメント形式のラムダを構築することはできません。 同じ機能を構築するために必要なコード例を次に示します。 while ループを作成するための API はなく、条件テストを含むループと、ループを抜けるためのラベル ターゲットを、開発者が作成する必要があります。

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 ステートメントなどの要素が多く含まれます。

このセクションでは、この式ツリーのすべてのノードにアクセスし、このサンプルで作成されたノードに関する情報を書き出すように、コードを記述しました。 GitHub の dotnet/docs レポジトリで、サンプル コードを表示またはダウンロードすることができます。 自分でサンプルをビルドし、実行してみてください。

コード コンストラクトを式にマップする

次のコード例では、API を使ってラムダ式 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# コンパイラのラムダ式から作成できる式ツリーよりも複雑な式ツリーを作成できます。 次の例は、数値の階乗を計算する式ツリーを作成する方法を示しています。

// 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.

詳細については、「Generating Dynamic Methods with Expression Trees in Visual Studio 2010 (Visual Studio 2010 での式ツリーによる動的メソッドの生成)」を参照し、Visual Studio 2010 以降のバージョンについても同じ方法を適用してください。