読者です 読者をやめる 読者になる 読者になる

ささいなことですが。

Windowsアプリテスト自動化ライブラリFriendly開発者の日記です。

Expression中の値を取り込む

C# Advent Calendar 2016の記事になります。26日ですが、今朝見たら空きがあったので書かせてもらうことにしました。
私は趣味でOSSのライブラリを作ってます。テスト自動化用のライブラリのFriendlyとか、最近はLambdicSqlというC#のラムダでSQLを作成するライブラリを作っています。
こんな感じでC#ラムダ式を書くと、そのまま文字列とパラメータになります。
Dapperと組み合わせるとそのまま実行可能です。
それからEntityFrameworkと組み合わせて使うこともできるようにしています。(β版リリース時の記事もご紹介。)

var min = 3000;

//C#からSQL作成
var query = Db<DB>.Sql(db =>

    //ここに書いたラムダが(割と)そのままSQLになる!
    Select(new SelectData()
    {
        Name = db.tbl_staff.name,
        PaymentDate = db.tbl_remuneration.payment_date,
        Money = db.tbl_remuneration.money,
    }).
    From(db.tbl_remuneration).
        Join(db.tbl_staff, db.tbl_staff.id == db.tbl_remuneration.staff_id).
    Where(min < db.tbl_remuneration.money && db.tbl_remuneration.money < 4000)
    
);

//文字列とパラメータ作成
var info = query.Build(_connection.GetType());
Debug.Print(info.Text);

//Dapperと連携可能。Select句の型情報を持っているので、そのまま実行できるよ。
var datas = _connection.Query(query).ToList();

こんなSQLになります。

SELECT
    tbl_staff.name AS Name,
    tbl_remuneration.payment_date AS PaymentDate,
    tbl_remuneration.money AS Money
FROM tbl_remuneration
    JOIN tbl_staff ON (tbl_staff.id) = (tbl_remuneration.staff_id)
WHERE ((@min) < (tbl_remuneration.money)) AND ((tbl_remuneration.money) < (@p_1))

例のようなシンプルなSQLだけではなくサブクエリとか内部結合とか、かなり複雑なSQLも記述可能です。
絶賛実装中なのです。当初の予定より遅れてますが、年明けくらいには1.0にする予定です。βにしたもののやってたら、やりたいこと増えていくんですよね・・・。

Expression中の値を取り込む

で、今回の本題です。Expression中の変数の取り込みに関してです。最小な感じにして以下で考えます。

var min = 100;
var query = Sql<DB>.Create(db => min < 4000);

端折りますが受けているところはこんな感じでexpression.Bodyから解析を開始します。

public static SqlExpression<TResult> Sql<TResult>(Expression<Func<TDB, TResult>> expression)
{
    //解析スタート
    expression.Body;
}

これを解析していくとこんな感じになっています。
BinaryExpression
  Left
    FieldExpression
      (min)
  Right
    ConstExpression
      Value (4000)

定数の取り込み

上の例で4000は定数です。これはコンパイル時に決まります。こういうのは解析していくとConstantExpressionという式で表されます。これは簡単でValueに値が入っているのでそれを取得できます。

object Convert(ConstantExpression constant)
	=>constant.Value;

変数の取り込み

これがポイントです。リフレクションを使うか式木をコンパイルしないと値が取れないんですよね。値が取れるとこまでExpressionを解析します。今回の場合だとすぐにConstExpressionになってValueにはminを保持しているobjectが取得できます。(本来は色々場合分けがありますが今回は端折ります)

object GetMemberObject(MemberExpression exp)
{
    var constant = member.Expression as ConstantExpression;
    var obj = constant.Value;
}

objの型は以下のものになっています。
{Test.Samples.<>c__DisplayClass73_0}
そんな型作った覚えないよって感じなのですが、コンパイル時に作られる型です。変数引き渡し用ですね。

で、objが取れるので
引数を一つとって、そのオブジェクトのメンバのminを返す式木を作ります。

var param = Expression.Parameter(obj.GetType(), "param");
var exp = Expression.PropertyOrField(param, member.Member.Name);

ここで問題なのは、objがobject型なので誰かがそれに静的な型を付与してやらないと、上手く呼び出せないことですね。キャストの式を入れても良いのですが、今回は Type Erasure というパターンを使うことにします。
静的な型が必要ないインターフェイスを用意し、それを実装するクラスが静的な型を持っているというものですね。それによって使う側は静的な型が必要なく使えます。

interface IGetter
{
    void Init(Expression exp, ParameterExpression[] param);
    object GetMemberObject(object[] arguments);
}
class GetterCore<T0> : IGetter
{
    public delegate object Func(T0 t0);
    Func _func;
    public void Init(Expression exp, ParameterExpression[] param) => _func = Expression.Lambda<Func>(exp, param).Compile();
    public object GetMemberObject(object[] arguments) => _func((T0)arguments[0]);
}
var getter = Activator.CreateInstance(typeof(GetterCore<>).MakeGenericType(type), true) as IGetter;
getter.Init(Expression.Convert(param, typeof(object)), new[] { param });
var value =  getter.GetMemberObject(new object[] { obj });

コンパイルすると遅い

このコンパイルはハイコストです。毎回やったら遅いですね。(最初は毎回やってたんですけどね・・・)なんでキャッシュします。上のではIGetterをキャッシュしておけばよいです。LambdicSqlではこの他にメソッド呼び出しとかオブジェクトの生成とかコンパイルが必要になるケースはすべてキャッシュするようにしました。それによってかなり高速にLambda→SQL変換ができるようになりました。(アドバイスをくれた皆様ありがとうございました!)

実はExpression化するだけでもコストがかかる

例えば、こんなコードでもタダではありません。私もやるまで「これはコンパイル時に決定されるよねー」とか勘違いしていたのですが、当然そんなことはなくExpressionの中に変数取り込んだりでコストは発生しているのです。しかも中の式が複雑になればなるほど、その時間は長くなるのです。

void Test()
{
    var min = 100;
    Empty(() => min < 4000);
}
static void Empty<T>(Expression<Func<T>> expression){}

つまり、がんばってパフォーマンスチューニングはしたものの限界はあります。多くのケースでは無視しても良いくらいなのですが、シビアなケースも考えLambadicSqlではユーザー側でキャッシュしやすい設計にしています。

Expression解析に興味があれば

github.com
結構Expression解析実装したのでサンプルになる部分もあるかも。興味があれば、見てみてください。
そんで、もうすぐ1.0リリースします。今もベータですが公開しているので興味があればNugetからご利用お願いします!
www.nuget.org

一日おくれですが、メリークリスマス!