VB.NETでも式木を扱いたい(その2)

jyuch.hatenablog.com

はじめに

今回も引き続き式木を扱っていきましょう。

そもそも式木とはなんでしょう。 という事でmsdnさんオナシャス

式ツリー (C# および Visual Basic)

アレでは上記の

  • コードをツリー状のデータ構造で表現できる
    • 実行時に動的に生成できる
      • そして実行できる
        • わりと速い
  • コードをデータ構造として解析する事ができる

というのを利用しています。

動的に実行するだけなら従来のリフレクションでもできますが、式木をコンパイルして実行した場合は倍近く速い(当社比)のでそれだけでも式木を使う理由になると思います。

プロパティ/フィールドの取得

アレはざっくり言うと以下の流れで処理を行っています。

  1. リフレクションを使ってクラスからプロパティ・フィールドをMemberInfoとしてぶっこぬく
  2. 1のMemberInfoを用いてそのクラスのインスタンスからそのメンバーを取得する式を式木で構築する
  3. 1、2をすべてのプロパティ・フィールドに対して行い、キャッシュする
  4. 3でキャッシュした式を用いてインスタンスからプロパティ・フィールドの値を取得し文字列形式を作成する

取り敢えず1はリフレクションなのでパスし、3は別問題なのでパスします。 と、言うわけで2からやっていきましょう。 前回に引き続きHogeクラスさんにご登場願いましょう。

Class Hoge
    Public Property MyProperty As Integer
    Public Property MyProperty2 As String
    Public MyField As String
End Class

このHogeからMyPropertyの値を取得する式木を構築して、実際に取得してみましょう。

紹介したmsdnのページにも書いてあったので気付いた方もいると思いますが、ラムダ式Expression(Of TDelegate)に突っ込めばコンパイラが式木を構築してくれるのでまずはそれを参考にしましょう。

Dim expr As Expression(Of Func(Of Object, Object)) = 
    Function(it) CType(it, Hoge).MyProperty

実際はコンパイル結果を逆コンパイルしたりクイックウォッチなどで中身を参考にしながら改変して目的のものを作るのですが、今回はこちらに完成済みのものを用意しました。

式木を構築するにはSystem.Linq.Expressions名前空間Expressionクラスの各静的メソッドを用います。

アレと同じにするならMemberInfoから式木を組み立てるのですが、わざわざ単体でMemberInfoを取り出すのも面倒なのでここではプロパティを文字列で指定してます。

Dim arg = Expression.Parameter(GetType(Object), "it")
Dim convertToHoge = Expression.Convert(arg, GetType(Hoge))
Dim getMemberValue = Expression.Property(convertToHoge, "MyProperty")
Dim convertToObject = Expression.Convert(getMemberValue, GetType(Object))
Dim lambda = Expression.Lambda(Of Func(Of Object, Object))(convertToObject, arg)
Dim expr = lambda.Compile()

Dim h = New Hoge() With {.MyProperty = 10}
Console.WriteLine(expr(h))

一行ずつ見てみましょう。

Dim arg = Expression.Parameter(GetType(Object), "it")Function(it)itを表します。

Dim convertToHoge = Expression.Convert(arg, GetType(Hoge))CType(it, Hoge)を表します。

Dim getMemberValue = Expression.Property(convertToHoge, "MyProperty")CType(it, Hoge).MyProperty.MyPropertyを表します。

Dim convertToObject = Expression.Convert(getMemberValue, GetType(Object))ラムダ式に明示的に書かれていませんが、CType(CType(it, Hoge).MyProperty, Object)CType(◯◯, Object)を表します。

最後のDim lambda = Expression.Lambda(Of Func(Of Object, Object))(convertToObject, arg)Func(Of Object, Object)型のデリゲートを表現しているFunction(◯◯) ◯◯を表しています。(言い方が変かな?)

これでHoge.MyPropertyにアクセスするコードを表現する式木の完成です。お疲れさま。 最後にコンパイルすれば晴れて実行可能なデリゲートが出来上がります。

Dim expr = lambda.Compile()
Dim h = New Hoge() With {.MyProperty = 10}
Console.WriteLine(expr(h))

これを繰り返してクラスのすべてのパブリックプロパティ・フィールドにアクセスするデリゲートを作成します。

ちなみになんでFunc(Of Object, Object)なのかと言うと、ToStringするだけなら別にObject型でも構わないからなるべく手間をかけたくないというちょっと乱暴な理由があります。

おわりに

今回は単一のプロパティの値を取得する式木について説明しました。

正直あとはリフレクションで取得したメンバーをフィルタしつつ同様に他のプロパティ・フィールドに対してもアクセッサを作るだけなので周りのごたごたが追加されるだけなのですが、次回はその辺の紹介をしたいと思います。

ちなみにアレではなんでプロパティだけでなくフィールドもサポートしているのかと言いますと、固定長ファイルフォーマットのマッパーライブラリにFileHelpersというものがあるのですがマッピング対象がパブリックフィールドという割とアバンギャルドな仕様になっておりまして、それに対応するために泣く泣くフィールドにも対応させたと言った感じなのです。*1 ですので、もともとはプロパティのみのサポートとするつもりだったのです。

とは言え、自前でマッピングするよりはずっと楽なので未だに固定長ファイルフォーマットと格闘しなければならない生産性アプリケーション開発者にはオススメです。まぁ、日本語の解説がほぼ無いのが欠点なのですが。

つづく

*1:v.1.0からv.1.1へのAPIの破壊的な変更のアレはこれが原因