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

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

はじめに

買ったゲームを二日で飽きる。どうも、弊社です。

前回は既知のクラスのメンバーにアクセスする式木を構築しましたが、既知のクラスにあんな面倒なことをやっても何も嬉しくありません。 やはり真価を発揮するのは未知のクラスのメンバーにアクセスしなければならないときです。

とは言え、サンプルコードとして突如出現する謎のクラスにアクセスしてもそれも嬉しくないので、またもやHogeクラス殿に登場願いましょう。

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

アレ

今回は前回の

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

の3以外を通しでやってみましょう。3は式木に直接的には関係ないので飛ばします。

ここでわざわざ直接的にはなんて強調しているのは、式木のデリゲートへのコンパイルのコストが馬鹿でかいので毎回式木を構築するのは非効率極まりない行為だからです。 なので、式木とキャッシュはほぼほぼセットです。

キャッシュについては最後に先っちょだけ触れます。

Sub Main()
    Dim h = New Hoge() With {.MyProperty = 1, .MyProperty2 = "Hoge"}
    h.MyField = "HogeHoge"
    ListupMembers(h)
End Sub

Private Sub ListupMembers(Of T)(obj As T)
    Dim exprs = InitAccessor(GetType(T))

    For Each it In exprs
        Console.WriteLine("{0}:{1}", it.Member.Name, it.Accessor(obj))
    Next
End Sub

Private Function InitAccessor(targetType As Type) As IEnumerable(Of MemberAccessor)
    Dim result = New List(Of MemberAccessor)
    For Each it In targetType.GetMembers(BindingFlags.Public Or BindingFlags.Instance)
        ' プロパティもしくはフィールド以外は無視
        If Not TypeOf it Is PropertyInfo AndAlso Not TypeOf it Is FieldInfo Then Continue For

        If TypeOf it Is PropertyInfo Then
            ' インデックス付きプロパティは無視
            If CType(it, PropertyInfo).GetIndexParameters.Length <> 0 Then Continue For

            ' 読めないものは無視
            If Not CType(it, PropertyInfo).CanRead Then Continue For
        End If

        Dim expr = GetMemberAccessor(targetType, it)
        result.Add(New MemberAccessor(it, expr))
    Next
    Return result
End Function

Private Function GetMemberAccessor(targetType As Type, member As MemberInfo) As Func(Of Object, Object)
    ' Function(it) CType(CType(it, targetType).Memver, Object)
    Dim arg = Expression.Parameter(GetType(Object), "it")
    Dim convToTarget = Expression.Convert(arg, targetType)
    Dim getMemberValue = Expression.MakeMemberAccess(convToTarget, member)
    Dim convToObject = Expression.Convert(getMemberValue, GetType(Object))
    Dim lambda = Expression.Lambda(Of Func(Of Object, Object))(convToObject, arg)
    Return lambda.Compile()
End Function

Private Class MemberAccessor
    Public ReadOnly Property Member As MemberInfo
    Public ReadOnly Property Accessor As Func(Of Object, Object)

    Public Sub New(info As MemberInfo, accessor As Func(Of Object, Object))
        Member = info
        Me.Accessor = accessor
    End Sub
End Class
MyProperty:1
MyProperty2:Hoge
MyField:HogeHoge

まず、各クラス及びメソッドについて説明します。

MemberAccessorクラスは各メンバー情報及びコンパイル済みのメンバーへのアクセス用のデリゲートを保持します。 たんなるコンテナですな。別にクラスを用意しなくてもTupleでも大丈夫です。

GetMemberAccessorMemberInfoからそのインスタンスのメンバーにアクセスするためのデリゲートを構築して返します。 メンバーの指定を文字列からMemberInfoクラスを用いるようになっただけで前回のとほとんど変わりません。

InitAccessorTypeクラスよりリフレクションを用い指定されたクラスのプロパティ及びフィールドを取り出し、各プロパティ及びフィールドにアクセスするための一連のMemberAccessorを構築します。

最後にListupMembersはクラスのプロパティ及びフィールドへの一連のアクセッサを元にインスタンスの内容を文字に起こします。

解説が必要なのはInitAccessorぐらいでしょうか。

targetType.GetMembers(BindingFlags.Public Or BindingFlags.Instance)

でメンバーをあらかじめパブリックなインスタンスメンバーにフィルタリングしています。 でないと、ここのHogeクラスには含まれてませんが静的メンバーも一緒に返され、式木を構築する段階で例外を吐きます。

あとは書き込みのみのプロパティとインデックス付きプロパティを弾いています。 書き込みのみは読めませんし、インデックス付きはインデックスを評価しないと値を取り出せません。

キャッシュ

ちらっと書きました、式木からデリゲートへコンパイルするコストはかなり大きいです。 ですので一回作ったデリゲートはなるべく使いまわした方がいいです。

正直正解は何か弊社も自信を持って言えませんが、アレではSystem.Collections.Concurrent.ConcurrentDictionary(Of TKey, TValue)を使用しています。

必ずシングルスレッドで動作することを保証できる(もしくは呼び出し元が保証する)場合はフツーのDictionary(Of TKey, TValue)で保持しても良いと思いますが、アレではそれを保証できないのでスレッドセーフを保証するためにConcurrentDictionaryを使っています。

Private Shared _cache As ConcurrentDictionary(Of Type, IEnumerable(Of MemberAccessor)) =
    New ConcurrentDictionary(Of Type, IEnumerable(Of MemberAccessor))

みたいな変数を用意して

Dim exprs As IEnumerable(Of MemberAccessor)
If Not _cache.TryGetValue(GetType(T), exprs) Then
    exprs = InitAccessor(GetType(T))
    _cache.TryAdd(GetType(T), exprs)
End If

みたいなことをやっています。

タイミングによっては2つ以上同じクラスにアクセスするための一連のアクセッサが作られどれか1つ以外は破棄されることになりますが、少なくとも初期化不良を起こして不正な状態の物がキャッシュされ続けることは無い・・・と思います。

おわりに

とりあえず3回に分けて式木からデリゲートを作成することをひたすら行いました。

今回構築している式木はかなり単純ですが、頑張ればそれなりに複雑な物も作れます。 が、やりたく無いです。いやだって、めんどうじゃん?

『出来るのとやるのは別問題』がモットー(?)の弊社としては、式木で作らなければならない最低限は式木で作り、それ以外は普通のやり方で簡単お手軽にやりたいのです。

おわり