【実務・中級編】VB.NETにおけるExpression(式ツリー)の動的構築:実行時にラムダ式を組み立てて高速なオブジェクトマッパーを作る – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

【伝説のアーキテクトが説く】リフレクションの呪縛を解け: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#以上の簡潔さで高度なメタプログラミングが可能だ。道具のせいにするな。言語の深淵を覗き込み、システムを支配せよ。

次回のコードレビューで、誰よりも速く、誰よりも堅牢なロジックを提示してみせろ。それがエンジニアとしての矜持だ。

タイトルとURLをコピーしました