【伝説のアーキテクトが説く】リフレクションの呪縛を解け:VB.NET 式ツリーによる爆速オブジェクトマッパーの構築
業務システム開発の現場において、「オブジェクト間の値コピー」は最も退屈で、かつパフォーマンスを損なうボトルネックになりがちだ。
多くのジュニアエンジニアは `PropertyInfo.GetValue` をループで回すリフレクションに頼る。あるいは、安易に `AutoMapper` のような重厚なライブラリを導入し、スタートアップ時間を肥大化させる。だが、真にシステムを理解するアーキテクトは、「実行時にコンパイルされるコード」を自ら生成する。
今日は、VB.NETの「式ツリー(Expression Trees)」を駆使し、リフレクションのオーバーヘッドをゼロに近づける動的マッパーの核心を伝授する。
—
1. なぜリフレクションは「罪」なのか
`System.Reflection` は強力だが、実行時にメタデータを読み解くコストは決して小さくない。数万件のレコードを処理するバッチ処理において、プロパティアクセスのたびにリフレクションを走らせることは、ガベージコレクションを誘発し、CPUサイクルをドブに捨てる行為に等しい。
我々が求めるのは、「あらかじめ人間が記述したかのようなIL(中間言語)を、実行時に生成してコンパイルする」というアプローチだ。一度生成したデリゲートをメモリ上にキャッシュすれば、以後はネイティブコードと同等の速度で動作する。
—
2. 実装:動的マッパー(MapperEngine)の設計
以下のコードは、ソースオブジェクトからターゲットオブジェクトへ値をコピーするデリゲートを動的に生成する、堅牢なエンジンだ。
Imports System.Linq.Expressions
Imports System.Reflection
Imports System.Collections.Concurrent
Public NotInheritable Class FastMapper(Of TSource, TTarget)
‘ 既に生成済みのデリゲートをキャッシュする(スレッドセーフ)
Private Shared ReadOnly _cache As New ConcurrentDictionary(Of String, Action(Of TSource, TTarget))()
Public Shared Sub Map(source As TSource, target As TTarget)
‘ キャッシュキーの生成(型名ベース)
Dim key = GetType(TSource).FullName & “->” & GetType(TTarget).FullName
Dim mapper = _cache.GetOrAdd(key, Function(k) CreateMapper())
mapper(source, target)
End Sub
Private Shared Function CreateMapper() As Action(Of TSource, TTarget)
Dim sourceParam = Expression.Parameter(GetType(TSource), “src”)
Dim targetParam = Expression.Parameter(GetType(TTarget), “dst”)
Dim bindings 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 AndAlso p.CanWrite)
If tProp IsNot Nothing Then
‘ dst.Property = src.Property の式を構築
Dim assignment = Expression.Assign(
Expression.Property(targetParam, tProp),
Expression.Property(sourceParam, sProp)
)
bindings.Add(assignment)
End If
Next
‘ 式ツリーをコンパイルしてデリゲート化
Return Expression.Lambda(Of Action(Of TSource, TTarget))(
Expression.Block(bindings), sourceParam, targetParam
).Compile()
End Function
End Class
—
3. この設計が「極限」である理由
1. キャッシュの徹底
`ConcurrentDictionary` を使用し、一度生成した実行ロジックを再利用している。式ツリーのコンパイルは重い処理だが、初回のみのコストとして割り切る。二回目以降は、手書きのコードと変わらない実行効率を誇る。
2. 型安全の担保
`Action(Of TSource, TTarget)` を介することで、コンパイル時に型チェックが行われる。リフレクションのような「実行時までタイポに気づかない」という悲劇は発生しない。
3. 保守性と拡張性
このコードは、型が一致しないケース(例:`String` から `Int` への変換)を含めていない。だが、ここを拡張すれば `Convert.ChangeType` を含めた式ツリーに書き換えることも容易だ。これが「動的構築」の真骨頂である。
—
4. 現場のアーキテクトからの忠告
- データベース連携時の注意:
DBから取得したDTOをドメインモデルに変換する際、このマッパーを多用すべきだ。ただし、複雑なエンティティグラフ(循環参照)がある場合は、単純なプロパティコピーでは限界が来る。その時は素直にシリアライザーや専用のORM(Entity Frameworkの `ProjectTo` など)を検討せよ。
- 例外処理の罠:
式ツリーの構築中に `PropertyType` が食い違っている場合、実行時コンパイルで `InvalidOperationException` が発生する。必ず `CanWrite` や型の互換性を事前に厳格にチェックするガード句を入れること。
- 「やりすぎ」の境界線:
この手法は、数万回・数十万回のループが走る箇所でのみ輝く。CRUDが主体のWeb画面でこれを導入しても、開発コストに見合うパフォーマンスは得られない。「どこがボトルネックか」を計測してから導入せよ。
結びに代えて
VB.NETは「古い」と軽視されがちだが、.NETの強力な型システムと式ツリーを使いこなせば、C#以上の簡潔さで高度なメタプログラミングが可能だ。道具のせいにするな。言語の深淵を覗き込み、システムを支配せよ。
次回のコードレビューで、誰よりも速く、誰よりも堅牢なロジックを提示してみせろ。それがエンジニアとしての矜持だ。
