【C#】Expression Treeでラムダ式を動的生成する

Expression Treeの基礎についてまとめてみました。

Expression Tree?

Expression Treeを使うとラムダ式を動的に生成できます。

では動的なコード生成ができると何ができるのかというと、
フィールドやプロパティ、メソッドなどを文字列から取得できます。

つまりReflectionのようなことができます。
そしてパフォーマンス面では悪名高きReflectionよりも優れています(これについてはパフォーマンスの節をご覧ください)。

C#のバージョンとExpression Tree

Expression TreeはC#3.5から使える機能ですが、
一部の機能はC#4.0からしか使えないので注意が必要です。

www.kekyo.net

とくに変数を書き換えるAssignメソッドがC#4.0からしか使えないのでご注意ください。
Reflection代わりに使うにはC#4.0以降の環境が実質必須と言えそうです。

iOS+iL2CPPとExpression Tree

執筆時点では、Expression TreeはiOS + IL2CPPでビルドするとランタイムエラーになります。
本記事とは直接関係ありませんが、Unityを使う読者も多いと思われるので一応注意喚起です。

https://forum.unity.com/threads/are-c-expression-trees-or-ilgenerator-allowed-on-ios.489498/

パフォーマンス

Reflectionよりパフォーマンスがかなりいいようです。

neue.cc

ds-optim.hateblo.jp

ufcpp.net

ただし実際にはReflectionもExpression Treeも普通はキャッシュするので、
そこまでパフォーマンスの差が気にならないケースもありそうです。

定数と引数とキャスト

ここからは実際に記述方法を見ていきます。
まずはごくシンプルなラムダ式を作ってみます。

// Func<int> func = () => 5;
var exp = Expression.Lambda<Func<int>>(
    Expression.Constant(5) // 定数の表し方
);

// コンパイルして実行
var lambda = exp.Compile();
Debug.Log(lambda.Invoke()); // 5

定数は上記のようにExpression.Constant()で表現します。
次にラムダ式に引数を渡す方法を見てみます。

// Func<int, int> func = x => x;
var arg1 = Expression.Parameter(typeof(int)); // 引数を定義
var exp = Expression.Lambda<Func<int, int>>(
    arg1, // 引数をそのまま返却
    arg1 // ラムダ式に渡す引数を指定
);

// コンパイルして実行
var lambda = exp.Compile();
Debug.Log(lambda.Invoke(7)); // 7

また、次のようにキャストが必要な場合には明示的に行う必要があります。

// Func<object, int> func = x => (int)x;
var arg1 = Expression.Parameter(typeof(object)); // object型の引数を定義
var exp = Expression.Lambda<Func<object, int>>(
    Expression.Convert(arg1, typeof(int)), // intにキャストして返す(キャストしないと例外発生)
    arg1 // ラムダ式に渡す引数
);

// コンパイルして実行
var lambda = exp.Compile();
Debug.Log(lambda.Invoke((object)9)); // 9

四則演算

四則演算はこんな感じです。

// Func<int, int> func = x => x + 5;
var arg1 = Expression.Parameter(typeof(int));
var exp1 = Expression.Lambda<Func<int, int>>(
    Expression.Add(arg1, Expression.Constant(5)),
    arg1
);

// Func<int, int> func = x => x - 5;
var arg2 = Expression.Parameter(typeof(int));
var exp2 = Expression.Lambda<Func<int, int>>(
    Expression.Subtract(arg2, Expression.Constant(5)),
    arg2
);

// Func<int, int> func = x => x * 5;
var arg3 = Expression.Parameter(typeof(int));
var exp3 = Expression.Lambda<Func<int, int>>(
    Expression.Multiply(arg3, Expression.Constant(5)),
    arg3
);

// Func<int, int> func = x => x / 5;
var arg4 = Expression.Parameter(typeof(int));
var exp4 = Expression.Lambda<Func<int, int>>(
    Expression.Divide(arg4, Expression.Constant(5)),
    arg4
);

// Func<int, int> func = x => x % 5;
var arg5 = Expression.Parameter(typeof(int));
var exp5 = Expression.Lambda<Func<int, int>>(
    Expression.Modulo(arg5, Expression.Constant(5)),
    arg5
);

比較演算

比較演算はこんな感じで表します。

// Func<int, bool> func = x => x == 5;
var arg1 = Expression.Parameter(typeof(int));
var exp1 = Expression.Lambda<Func<int, bool>>(
    Expression.Equal(arg1, Expression.Constant(5)),
    arg1
);

// Func<int, bool> func = x => x != 5;
var arg2 = Expression.Parameter(typeof(int));
var exp2 = Expression.Lambda<Func<int, bool>>(
    Expression.NotEqual(arg2, Expression.Constant(5)),
    arg2
);

// Func<int, bool> func = x => x < 5;
var arg3 = Expression.Parameter(typeof(int));
var exp3 = Expression.Lambda<Func<int, bool>>(
    Expression.LessThan(arg3, Expression.Constant(5)),
    arg3
);

// Func<int, bool> func = x => x <= 5;
var arg4 = Expression.Parameter(typeof(int));
var exp4 = Expression.Lambda<Func<int, bool>>(
    Expression.LessThanOrEqual(arg4, Expression.Constant(5)),
    arg4
);

// Func<int, bool> func = x => x > 5;
var arg5 = Expression.Parameter(typeof(int));
var exp5 = Expression.Lambda<Func<int, bool>>(
    Expression.GreaterThan(arg5, Expression.Constant(5)),
    arg5
);

// Func<int, bool> func = x => x >= 5;
var arg6 = Expression.Parameter(typeof(int));
var exp6 = Expression.Lambda<Func<int, bool>>(
    Expression.GreaterThanOrEqual(arg6, Expression.Constant(5)),
    arg6
);

参考

ufcpp.net

neue.cc

ds-optim.hateblo.jp

ufcpp.net