滅入るんるん

何か書きます

C# 式ツリーのこね方

式ツリーサンプル

using System;
using System.Linq.Expressions;

public class Program
{
    public static void Main(string[] args)
    {
        var program = new Program();
        int[] values = new[] { 1, 5, 2, 6, 7, 8, 9, 3, 2 };
        Console.WriteLine($"Sum: {program.Sum(values)}");
        Console.WriteLine($"SumFlow: {program.SumFlow(values)}");
        Console.WriteLine($"SumExpression: {program.SumExpression()(values)}");
        // Sum: 43
        // SumFlow: 43
        // SumExpression: 43
    }

    public int Sum(int[] values)
    {
        int result = 0;
        for (int i = 0; i < values.Length; i++)
        {
            result += values[i];
        }
        return result;
    }

    public int SumFlow(int[] values)
    {
        int result = 0;
        int i = 0;
        for (; ; )
        {
            if (values.Length <= i)
            {
                break;
            }
            result += values[i];
            i++;
        }
        return result;
    }

    public Func<int[], int> SumExpression()
    {
        ParameterExpression values = Expression.Parameter(typeof(int[]), "values");

        ParameterExpression result = Expression.Variable(typeof(int), "result");
        BinaryExpression resultAssign = Expression.Assign(result, Expression.Constant(0, typeof(int)));

        ParameterExpression i = Expression.Variable(typeof(int), "i");
        BinaryExpression iAssign = Expression.Assign(i, Expression.Constant(0, typeof(int)));

        LabelTarget endLoop = Expression.Label("endloop");
        LoopExpression loop = Expression.Loop(
            Expression.Block(
                Expression.IfThen(
                    Expression.LessThanOrEqual(Expression.PropertyOrField(values, "Length"), i),
                    Expression.Break(endLoop)
                ),
                Expression.AddAssign(result, Expression.ArrayIndex(values, i)),
                Expression.AddAssign(i, Expression.Constant(1, typeof(int)))
            ),
            endLoop
        );

        BlockExpression block = Expression.Block(
            typeof(int),
            new[] { result, i },
            resultAssign,
            iAssign,
            loop,
            result
        );
        return Expression.Lambda<Func<int[], int>>(block, values).Compile();
    }
}

仕組みをだらだらと書くだけでわかりにくいので、int配列の合計値をintで返すメソッドを式ツリー化して考えてみましょう。 (オーバーフローのこととか考えてないけど許して)

Mainメソッドはエントリーポイントなので割愛。 今回式ツリー化するメソッドとしてSumメソッドを用意しました。実際に式ツリーを運用()する際にはSumメソッドを定義する必要はありませんが、式ツリーをコードだけで表現するとわけがわからなくなるので、一度式ツリー化したいメソッドを用意してやるといいでしょう。

今回はfor文を式ツリー化したいのですが、Sumメソッドをそのままの形でfor文を式ツリー化することはできないので、ちょっとレガシーっぽい形にする必要があります。それを表現してるのがSumFlowメソッドです。

さて、本題の式ツリー化ですが、実際のロジックはSumExpressionメソッドに記述しています。上から順にみていきましょう。

Sumメソッドでは引数にint配列のvaluesを取っています。それを表現しているのがExpression.Parameter(typeof(int[]), "values")です。 次にint型のresultとiを初期値0で宣言していますが、これらは宣言と代入に分割します。Expression.Variable(typeof(int), "result")で宣言し、Expression.Assign(result, Expression.Constant(0, typeof(int)))で代入を行っています。 for文は単なるループに置き換えられます。Expression.Label("endloop")でループを抜けるラベルを宣言し、Expression.Break(endLoop)でループを抜けます。 もう少し説明すると、Expression.Loopの第一引数でExpression.Break入りのExpression.Blockを渡し、第二引数でループを抜けるラベルを渡してやります。 (ループ内の処理は見た目そのままだから割愛)

Expression.Lambda<Func<int[], int>>でラムダを作成する前にラムダ内の処理をブロックでまとめる必要がありますが、ここが躓きポイントです。 Expression.Blockの第一引数にラムダの返り値の型、第二引数に宣言する変数のParameterExpressionの配列(今回はresultとi)、第三引数以降は実際の処理ですが、最終引数にreturnする値(今回はresult)を渡す必要があります。 そこまで記述できたらあとはCompileするだけです!

以下、未整理

// object x => {}
// 引数を表す
ParameterExpression x = Expression.Parameter(typeof(object), "x");
// Task task;
// 変数を表す
ParameterExpression task = Expression.Variable(typeof(Task), "task");
// task = Task.CompletedTask;
// Expression.Constant(Task.CompletedTask)で式ツリーをこねる際の変数などを
// 式ツリー上に持っていくことができる
BinaryExpression taskAssignDefault = 
    Expression.Assign(task, Expression.Constant(Task.CompletedTask));
// navigationRequest == item.request
// navigationRequestはParameterExpression
BinaryExpression test = Expression.Equal(navigationRequest, Expression.Constant(item.request));
// (T)x
// キャスト
// methodArgument1TypeはType型
UnaryExpression castedX = Expression.Convert(x, methodArgument1Type);
// instance.method<T>((T)x);
// メソッドを表現する
// typeArgumentsはType[]型、methodArgumentsはExpression[]型
MethodCallExpression method = 
    Expression.Call(instanceExpression, item.method.name, typeArguments, methodArguments);
// task = instance.method<T>((T)x);
BinaryExpression taskAssignResult = Expression.Assign(task, method);
// if(x == item.request) task = instance.method((T)x);
ConditionalExpression ifStatement = Expression.IfThen(test, taskAssignResult);
// Taskが返り値なブロック
// statements最後の要素はTask型を表すParameterExpression
BlockExpression block = Expression.Block(typeof(Task), new[] { task }, statements);
// object x, INavigationRequest navigationRequest => {}
// 実行可能なデリゲートにする
Expression.Lambda<Func<object, INavigationRequest, Task>>(block, x, navigationRequest).Compile();

未整理部コード全文は https://github.com/MeilCli/Kamihikouki/blob/master/Kamihikouki.NETStandard/CachedNavigator.cs にあります。 コードサンプル量が増えたら整理します。