実行時の「魔法」:Expression Treeでリフレクションの呪縛を解き放つ
現場の老兵たちは皆知っている。`System.Reflection`がもたらす柔軟性は、往々にしてパフォーマンスという名の「代償」を要求する。特に、数万件のレコードを処理する基幹系システムにおいて、`PropertyInfo.GetValue`のループは地獄の入り口だ。
VB.NETを極めようとするなら、そろそろ「リフレクションを直接叩く」という愚行を卒業すべきだ。今日は、実行時にIL(中間言語)を生成し、コンパイルされたコードと同等の速度でオブジェクトのマッピングを行う「Expression Tree(式ツリー)」の極意を伝授する。
—
1. なぜリフレクションは遅いのか?
リフレクションは、実行時に型メタデータを検索し、呼び出し可能なメソッドを特定する。この「検索」というオーバーヘッドは、頻繁に呼び出されるマッパーにおいては無視できない。
真のプロフェッショナルは、「一度だけ」動的にコードを生成し、それをキャッシュする。 これが、Expression Treeが提供するメタプログラミングの核心だ。
—
2. 高速オブジェクトマッパーの構築
以下のコードは、ソースオブジェクトの全プロパティをターゲットへコピーするデリゲートを、実行時に動的に構築するファクトリの実装例だ。
Imports System.Linq.Expressions
Imports System.Reflection
Public Class FastMapper(Of TSource, TTarget)
‘ 生成したデリゲートをキャッシュ(これが高速化の鍵)
Private Shared ReadOnly _mapper As Action(Of TSource, TTarget) = CompileMapper()
Public Shared Sub Map(source As TSource, target As TTarget)
_mapper(source, target)
End Sub
Private Shared Function CompileMapper() As Action(Of TSource, TTarget)
Dim sourceParam = Expression.Parameter(GetType(TSource), “src”)
Dim targetParam = Expression.Parameter(GetType(TTarget), “tgt”)
Dim expressions As New List(Of Expression)()
‘ 両クラスのプロパティを突き合わせる
Dim sourceProps = GetType(TSource).GetProperties()
Dim targetProps = GetType(TTarget).GetProperties()
For Each sProp In sourceProps
Dim tProp = targetProps.FirstOrDefault(Function(p) p.Name = sProp.Name AndAlso p.PropertyType = sProp.PropertyType)
If tProp IsNot Nothing AndAlso tProp.CanWrite Then
‘ tgt.Prop = src.Prop を構築
Dim assignment = Expression.Assign(
Expression.Property(targetParam, tProp),
Expression.Property(sourceParam, sProp)
)
expressions.Add(assignment)
End If
Next
‘ ブロックとして統合し、ラムダ式に変換してコンパイル
Dim block = Expression.Block(expressions)
Return Expression.Lambda(Of Action(Of TSource, TTarget))(block, sourceParam, targetParam).Compile()
End Function
End Class
この設計の凄み
- Compile()の力: `Compile()`が実行された瞬間、式ツリーはILへと変換される。初回呼び出し時のコストは高いが、以降は通常のメソッド呼び出しと変わらない速度で動作する。
- メモリの最適化: 構築されたデリゲートは`Static ReadOnly`で保持される。これにより、ガベージコレクションを誘発する不要なオブジェクト生成を排除している。
—
3. レガシー現場での「禁じ手」と「流儀」
この技術を既存のVBA/VB6ベースのシステムと連携させる場合、以下の点に留意せよ。
1. Windows API呼び出しとの境界線:
P/Invokeを行う際、構造体のマーシャリングは高コストだ。マッピングの結果をメモリ上に固定(`GCHandle`)してポインタを渡す際、不要なコピーが発生していないか、常に`Marshal.StructureToPtr`の挙動を監視せよ。
2. Disposeの厳格化:
式ツリーで生成されたデリゲート自体はマネージコードだが、もしマッピング先がCOMオブジェクトやネイティブハンドルを保持している場合、`.NET`側のGCに依存せず、明示的に`Marshal.ReleaseComObject`を呼ぶこと。VB.NETの`Using`ブロックは、時に「甘え」となる。本当にリソースを解放すべきタイミングをコードが理解しているかを確認せよ。
3. レガシー保守の極意:
古いVB.NETプロジェクトを改修する際、無理にすべてを最新のDIコンテナやライブラリに置き換える必要はない。ボトルネックとなっている「ループ処理」だけを、このようなExpression Treeで書き換える。これが、システムを殺さずにパフォーマンスを劇的に改善する「外科手術」の流儀だ。
—
結びに代えて
多くの開発者がリフレクションの利便性に甘んじ、パフォーマンスを犠牲にしている間に、我々アーキテクトは「実行時のコード生成」という武器で静かに差をつける。
VB.NETは古い言語ではない。.NETの強力なランタイムの上に君臨する、極めて柔軟なメタプログラミング言語だ。この知識を武器に、あなたのシステムに「コンパイル時と同等の速度」という真の優雅さを与えてほしい。
健闘を祈る。
