VB.NETの深淵へ:Expression Treeで実現する「リフレクション超え」の高速マッパー構築術
こんにちは。現場で長年VB.NETと心中してきた、アーキテクトの先輩です。
多くのVBプログラマは、オブジェクトの値を別のクラスにコピーする際、安易に`Reflection`(リフレクション)を使います。`PropertyInfo.GetValue`をループで回すアレです。でも、もしあなたが「パフォーマンスこそが正義」という現場にいるなら、リフレクションのオーバーヘッドは看過できないボトルネックになります。
今回は、VB.NETが持つ最強の武器の一つ「Expression Tree(式ツリー)」を使って、実行時にコードをコンパイルし、静的コードと同等の速度を叩き出す「動的オブジェクトマッパー」の作り方を伝授します。
—
1. なぜ「リフレクション」では満足できないのか?
リフレクションは、実行時に型の情報を解析する「探査型」の処理です。これは非常に便利ですが、実行のたびにメタデータを検索するため、CPU負荷が高くなります。
一方で、Expression Treeは「命令の設計図」を動的に組み立て、それをメモリ上でIL(中間言語)にコンパイルする手法です。一度コンパイルしてデリゲート(関数)としてキャッシュしてしまえば、実行速度は静的なコードとほぼ変わりません。
—
2. Expression Treeの基本概念
Expression Treeとは、コードを「データ」として扱う仕組みです。
- 静的なコード: `Dim x = a + b` (書いた瞬間に固定される)
- Expression Tree: 「aとbを足す」という命令をツリー構造のオブジェクトとしてメモリ上に持ち、好きなタイミングで「実行」する。
これを活用して、実行時に「SourceクラスからTargetクラスへ値をコピーする命令」を組み立てます。
—
3. 【実践】動的オブジェクトマッパーの実装
まずは、リフレクションの呪縛から解き放たれるコードを見てください。ここでは、プロパティ名を照合して値をコピーするマッパーを作ります。
Imports System.Linq.Expressions
Imports System.Reflection
Public Class FastMapper(Of TSource, TTarget)
‘ コンパイル済みの高速デリゲートをキャッシュ
Private Shared ReadOnly _mapper As Action(Of TSource, TTarget) = CompileMapper()
Private Shared Function CompileMapper() As Action(Of TSource, TTarget)
Dim sourceParam = Expression.Parameter(GetType(TSource), “src”)
Dim targetParam = Expression.Parameter(GetType(TTarget), “dst”)
Dim expressions As New List(Of Expression)()
‘ プロパティの突き合わせ
For Each srcProp In GetType(TSource).GetProperties()
Dim dstProp = GetType(TTarget).GetProperty(srcProp.Name)
‘ 名前が一致し、書き込み可能なら代入式を生成
If dstProp IsNot Nothing AndAlso dstProp.CanWrite Then
‘ dst.Prop = src.Prop
Dim getter = Expression.Property(sourceParam, srcProp)
Dim setter = Expression.Property(targetParam, dstProp)
Dim assign = Expression.Assign(setter, getter)
expressions.Add(assign)
End If
Next
‘ ブロックにまとめてコンパイル
Dim block = Expression.Block(expressions)
Return Expression.Lambda(Of Action(Of TSource, TTarget))(block, sourceParam, targetParam).Compile()
End Function
‘ 呼び出し用メソッド
Public Shared Sub Map(source As TSource, target As TTarget)
_mapper(source, target)
End Sub
End Class
このコードの「賢い」ポイント
1. 静的コンストラクタの活用: `Shared ReadOnly`で初期化することで、プログラム実行中に一度だけコンパイルが走ります。2回目以降は、ネイティブコードと同等の速度で実行されます。
2. Expression.Block: 複数の代入処理を一つの命令ブロックとしてまとめ、一気にコンパイルすることで効率を最大化しています。
3. 型の安全性: ジェネリクス(`Of TSource, TTarget`)を使うことで、コンパイル時に型の整合性を保証しつつ、実行時は型情報を意識させない柔軟性を持たせています。
—
4. 陥りやすい罠と解決策
この世界に足を踏み入れると、いくつか壁に当たります。
- 型変換エラー: `Source`が`Integer`で`Target`が`Long`の場合、単純な`Assign`ではエラーになります。その際は `Expression.Convert` を使い、明示的なキャストをツリーに組み込んでください。
- デバッグの難しさ: Expression Treeは動的生成のため、エラーが発生してもスタックトレースが追いづらいです。最初は小さなクラスでテストし、徐々に複雑なネスト構造に対応させるのがコツです。
—
最後に:エンジニアとしての心構え
「標準の機能で間に合うなら、それを使うのが一番良い」
これが、私がVB.NETを極めた末に出した結論です。しかし、どうしてもパフォーマンスが必要なとき、ライブラリに頼らず自分自身で「コードを生成するコード」を書く能力は、あなたを他のエンジニアとは一線を画す存在にします。
VB.NETは古い言語だと言う人もいますが、.NETの強力なメタプログラミング機能を最も直感的に扱える言語の一つであることに変わりはありません。
さあ、この「式ツリー」を武器に、あなたのアプリケーションを限界まで高速化させてみてください。もし分からないことがあれば、いつでもまた聞きに来てくださいね。応援しています。
