LoginSignup
13
13

More than 5 years have passed since last update.

Expressionを使って動的コード生成するための逆引きメモ

Last updated at Posted at 2019-03-06

概要

リフレクションは遅いので、それを少しでも高速化できればと式木について調べたので、逆引き的な感じでやり方をメモしておこうと思います。
(なので新しいものとかあったら随時更新予定)

なお、基礎的なところは以下の記事が色々と比較しながら記載してくれているので参考にしてみてください。
本記事は、ある程度知識がある状態で、「これはどうしたらいいんだ?」というのを解決するためのメモです。

引数を使う

まずは簡単に、引数をふたつ取ってそれを加算し返すだけのシンプルな式木を作成してみます。

// int型の引数 "x"
ParameterExpression x = Expression.Parameter(typeof(int), "x");

// int型の引数 "y"
ParameterExpression y = Expression.Parameter(typeof(int), "y");

// ふたつの引数を加算する命令
BinaryExpression body = Expression.Add(x, y);

// int型の引数を2つ取って、int型の戻り値を持つラムダ式。
Expression<Func<int, int, int>> lambda = Expression.Lambda<Func<int, int, int>>(body, x, y);

// 生成したラムダ式をコンパイルする
Func<int, int, int> del = lambda.Compile();

// 実際に実行する
Console.WriteLine(del(1, 2)); // => 3

配列を使う

次に、配列とそのindexを引数に渡して合計するサンプルを作ってみます。

// 配列のindexを引数で受け取る
ParameterExpression ind = Expression.Parameter(typeof(int), "ind");

// 配列自体への参照
ParameterExpression arr = Expression.Parameter(typeof(float[]), "arr");

// 合計結果を計算し、リターンするための引数
ParameterExpression res = Expression.Parameter(typeof(float), "res");

// 配列へのアクセス `arr[ind]` 相当。
var left = Expression.ArrayIndex(arr, ind);

// 引数の現在の合計結果を右辺にするために分かりやすく名前をつける
var right = res;

// 配列要素と引数の値を加算する命令
BinaryExpression body = Expression.Add(left, right);

// func(float[] array, int index, float result) を実現するラムダ式を生成、コンパイル
var func = Expression.Lambda<Func<float[], int, float, float>>(body, arr, ind, res).Compile();

// 計算をテストするための配列
float[] data = new [] { 1f, 2f, 3f, 4f, 5f, };

// 結果格納変数
float result = 0;
for (int i = 0; i < data.Length; i++)
{
    var d = data[i];
    // ここで実際に計算を行う
    result = func(data, i, result);
}

Console.WriteLine(result);

インスタンスを生成(new)する

さて、もう少し実践的なところで、インスタンスを生成する処理を見てみます。

NewExpression body = Expression.New(typeof(HogeClass));
LambdaExpression lambda = Expression.Lambda<Func<HogeClass>>(body);

Func<HogeClass> func = (Func<HogeClass>)lambda.Compile();
HogeClass hoge = func();

シンプルですね。インスタンスの生成には Expression.New を利用し、引数には Type 型の値を渡します。

引数を伴ったインスタンス生成

さて、次は引数付きのコンストラクタを持つクラスのインスタンス化についてです。
通常、引数を伴わないコンストラクタだけでプログラムをすることはまずないと思うので、引数を扱うのは必須ですね。

// ラムダ式で引数を受け取るためのパラメータを定義
ParameterExpression arg = Expression.Parameter(typeof(int), "arg");

// 引数付きコンストラクタの場合は、Type型ではなく、ConstructorInfoを用いてインスタンス化するため、引数をひとつ受け取るコンストラクタ情報を取得する
ConstructorInfo ctor = typeof(HogeClass).GetConstructor(new [] { typeof(int) });
// 以下のような書き方もある
// ConstructorInfo ctor = typeof(HogeClass).GetConstructor(BindingFlags.Instance | BindingFlags.Public, null, CallingConventions.HasThis, new [] { typeof(int) }, new ParameterModifier[0]);

// コンストラクタ情報と引数パラメータを指定してインスタンス生成式を生成
NewExpression instance = Expression.New(ctor, arg);

// ラムダ式を生成
LambdaExpression lambda = Expression.Lambda<Func<int, HogeClass>>(instance, arg);
Func<int, HogeClass> func = (Func<int, HogeClass>)lambda.Compile();

// 実際に実行する
HogeClass hoge = func(50);

初期化子を伴ったインスタンス生成

上記のインスタンス生成は、引数なし/引数ありのコンストラクタを呼び出す処理でした。
それとは別に、初期化子を伴ったインスタンスの生成は以下のようになります。

// インスタンス生成式
NewExpression instance = Expression.New(typeof(HogeClass));

// Bindを利用して、インスタンス生成時に初期化したいプロパティについての情報を取得、定数の設定を行う式を構築
MemberAssignment bind = Expression.Bind(typeof(HogeClass).GetMember("AnyProperty")[0], Expression.Constant(100));

// 実際の初期化処理
Expression expr = Expression.MemberInit(instance, bind);

// ラムダ式の生成
LambdaExpression lambda = Expression.Lambda<Func<HogeClass>>(expr);
Func<HogeClass> func = (Func<HogeClass>)lambda.Compile();

// 実際に実行
HogeClass hoge = func();

プロパティ/フィールドにアクセスする

今度は該当クラスのプロパティ/フィールドにアクセスする式木を構築してみます。
なお、public / private 関係なくアクセスできるようです。

ちなみに以下のような式の想定です。

HogeClass hoge = new Hoge("edo");
string result = hoge._name; // => Expressionを使うとprivateでもアクセスできる
// 対象となるクラスを引数で受け取るためのパラメータ
ParameterExpression arg = Expression.Parameter(typeof(HogeClass), "target");

// プロパティアクセスのための式
MemberExpression prop = Expression.PropertyOrField(arg, "_name");

// ラムダ式の生成
LambdaExpression lambda = Expression.Lambda<Func<HogeClass, string>>(prop, arg);
Func<HogeClass, string> func = (Func<HogeClass, string>)lambda.Compile();

// 実際に実行
HogeClass hoge = new HogeClass("edo");
string result = func(hoge);

Console.WriteLine(result); // => "edo"

インスタンスを生成し、プロパティなどに値を設定してインスタンスを返す

こちらの例は、コンストラクタ引数や初期化子などを利用せず、生成したインスタンスに対してプロパティ経由で値を設定して、その生成したインスタンスを返す、というケースです。
イメージ的には以下のようなコードを、式木を使って生成します。

HogeClass hoge = new HogeClass();
hoge.AnyProperty = 33;
return hoge;

なお、こちらのコードは以下の記事を参考にさせていただきました。

// 生成するインスタンスの型
Type ctorType = typeof(HogeClass);

// 引数で受け取る値のパラメータ
ParameterExpression propParam = Expression.Parameter(typeof(int), "propParam");

// インスタンス生成式
NewExpression ctor = Expression.New(ctorType);

// 生成したインスタンスを参照するローカル変数
ParameterExpression local = Expression.Parameter(ctorType, "instance");

// インスタンスのプロパティアクセス
MemberExpression prop = Expression.Property(local, "AnyProperty");

// 戻り値の型
LabelTarget returnTarget = Expression.Label(ctorType);

// 戻り値の設定
GotoExpression returnExpr = Expression.Return(returnTarget, local, ctorType);

// ちょっとこれの理由は不明・・・
LabelExpression returnLabel = Expression.Label(returnTarget, Expression.Default(ctorType));

// 上記処理を行うブロック文
BlockExpression body = Expression.Block(
    new[] { local },
    Expression.Assign(local, ctor),
    Expression.Assign(prop, propParam),
    returnExpr,
    returnLabel
);

// ラムダ式の生成
LambdaExpression lambda = Expression.Lambda<Func<int, HogeClass>>(body, propParam);
var func = (Func<int, HogeClass>)lambda.Compile();

// 結果を受け取る
var result = func(33);

Console.WriteLine(result);
Console.WriteLine(result.AnyProperty);

インスタンスをキャストする

引数でベースクラスを受け取り、それを使用したいクラスにキャストして利用する、みたいなケースです。
本来はあまりお行儀のよくない処理ですが、今回実装にチャレンジしているメタプログラミングはあるシステムを構築する場合なので、利用ケースは限定的であり、安全に利用できる前提での話となります。

キャストにはExpression.Callを利用する

Expression.Callを利用してConvertクラスのメソッドを呼び出します。

なお、今回利用したのは以下のシグネチャを持つメソッドです。(→ドキュメント

public static System.Linq.Expressions.MethodCallExpression Call (Type type, string methodName, Type[] typeArguments, params System.Linq.Expressions.Expression[] arguments);
// メソッドの引数に対象クラスのTypeをもらう想定

// ラムダ式の引数として与えられるパラメータ
ParameterExpression arg1 = Expression.Parameter(typeof(AnyBase), "arg1");
ParameterExpression arg2 = Expression.Parameter(typeof(AnyBase), "arg2");

// ローカルで利用するためのローカル変数パラメータ
ParameterExpression local1 = Expression.Parameter(type, "instance1");
ParameterExpression local2 = Expression.Parameter(type, "instance2");

// 引数をキャストする
MethodCallExpression convertExpr1 = Expression.Call(typeof(Convert), "ChangeType", null, arg1, Expression.Constant(local1.Type));
UnaryExpression valueCast1 = Expression.Convert(convertExpr1, local1.Type);

MethodCallExpression convertExpr2 = Expression.Call(typeof(Convert), "ChangeType", null, arg2, Expression.Constant(local2.Type));
UnaryExpression valueCast2 = Expression.Convert(convertExpr2, local2.Type);

// キャストした引数をローカル変数にアサインする
BinaryExpression castAssign1 = Expression.Assign(local1, valueCast1);
BinaryExpression castAssign2 = Expression.Assign(local2, valueCast2);

余談ですが、Unaryは「単項」という意味の単語なので、それ単体で意味を成す式、という感じでしょうか。

13
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
13