参照地獄からの脱却:VB.NETにおける堅牢なディープコピー実装術
業務システムや自動化ツールを開発していて、こんな怪奇現象に悩まされたことはないか?
> 「画面の入力値を保持用オブジェクトに退避させたはずなのに、後続の処理でデータを書き換えたら、なぜか退避させたはずの元データまで変わってしまった……!」
原因は明確だ。あなたが使ったのは、メモリのアドレス(参照)をコピーしただけのシャローコピー(浅いコピー)に過ぎない。オブジェクトがネスト(階層構造)を持つ複雑なデータ構造である場合、VB.NET標準の仕組みや安易な代入は、予期せぬバグの温床となる。
今回は、VB.NETの標準インターフェースである `ICloneable` の抱える致命的な欠陥を暴き、実務の現場で絶対に破綻しない「真のディープコピー(深いコピー)」の設計と実装手法を叩き込む。
—
なぜ `ICloneable` は実務で使ってはいけないのか?
.NETの歴史的遺産である `ICloneable` インターフェース。これを見た瞬間、「とりあえずこれを実装しておけばいいや」と思考停止で `MemberwiseClone()` を返すコードを書くジュニアエンジニアが後を絶たない。
しかし、シニアエンジニアの視点から言えば、`ICloneable` は百害あって一利なしのアンチパターンだ。
1. 型安全性の欠如(`Object` 型の呪い)
`ICloneable.Clone()` メソッドの戻り値は `Object` 型だ。呼び出し側で必ず `DirectCast` や `CType` によるキャストが必要になり、コンパイル時の型安全性がスポイルされる。
2. 「ディープかシャローか」の仕様が曖昧
インターフェースのシグネチャには「それが浅いコピーを返すのか、深いコピーを返すのか」を定義する契約(Contract)が存在しない。実装者によって挙動が異なるため、保守性を著しく低下させる。
3. 参照型のネストに完全無力
`MemberwiseClone` は、自クラスのフィールドをコピーするが、フィールドが参照型(他のクラスやListなど)である場合、参照先のアドレスしか複製しない。結果として、複製元のオブジェクトと複製先のオブジェクトが、内部のコレクションや子オブジェクトを「共有」してしまうことになる。
—
堅牢なディープコピーを実装する2つのアプローチ
実務でネストしたオブジェクトを完全に切り離して複製するには、主に以下の2つのアプローチが存在する。
1. シリアライゼーション(直列化)を利用する方法(最も手っ取り早く、階層が深くても破綻しない)
2. 手動による再帰的ディープコピー(パフォーマンス重視、DBエンティティ等で確実性を求める場合)
今回は、保守性が高く、メンテナンスの手間が少ない「JSONシリアライゼーションを利用したモダンなディープコピー」と、パフォーマンスと安全性を両立する「手動再帰実装」の双方をプロダクションコードで提示しよう。
—
プロダクションコード:安全なオブジェクト複製の全貌
以下のコードは、顧客情報(Customer)とそれに紐づく住所(Address)、注文履歴(List
Imports System.IO
Imports System.Text.Json
Namespace BusinessLogic.Utilities
‘ =========================================================================
‘ データモデルの定義(ネスト構造を持つ実務的サンプル)
‘ =========================================================================
”’
”’
Public Class Customer
Public Property Id As Integer
Public Property Name As String
Public Property ShippingAddress As Address ‘ 参照型(子オブジェクト)
Public Property OrderHistory As List(Of Order) ‘ 参照型(コレクション)
”’
”’
Public Function DeepCopy() As Customer
‘ JSON文字列に一度シリアライズし、即座にデシリアライズすることで
‘ メモリ上の参照関係を完全に切り離した完全な別インスタンスを生成する
Dim jsonOptions As New JsonSerializerOptions With {
.PropertyNameCaseInsensitive = True
}
Dim jsonString As String = JsonSerializer.Serialize(Me, jsonOptions)
Return JsonSerializer.Deserialize(Of Customer)(jsonString, jsonOptions)
End Function
End Class
”’
”’
Public Class Address
Public Property ZipCode As String
Public Property Prefecture As String
Public Property City As String
End Class
”’
”’
Public Class Order
Public Property OrderId As String
Public Property Amount As Decimal
Public Property OrderDate As Date
End Class
End Namespace
この実装が優れている理由
`System.Text.Json` を利用したシリアライズ手法は、オブジェクトの階層がどれほど深くても、循環参照の対策(設定による)さえ行えば、プロパティの追加・変更に対してコードを修正する必要がない。保守性において圧倒的な強みを発揮する。
—
パフォーマンス・DB・ファイル連携における実務上の注意点
業務自動化ツールやエンタープライズシステムでオブジェクトコピーを行う際、以下の罠に注意しなければならない。
1. データベースエンティティ(Entity Framework等)のコピー
ORM(Object Relational Mapper)が管理しているエンティティをそのままJSONシリアライズすると、Lazy Loading(遅延読み込み)のプロキシや循環参照(Parent -> Child -> Parent)で例外が発生する。
DBエンティティを複製する場合は、UI層やビジネスロジック層専用の「DTO(Data Transfer Object)」にマッピングした上でコピーするか、以下のように手動でインスタンスを組み立てる必要がある。
2. 処理速度のボトルネック
JSONシリアライズは非常にスマートだが、数万件のオブジェクトをループ内で何度もディープコピーすると、ガベージコレクション(GC)の負荷が高まり、パフォーマンスが劣化する。
高頻度で呼ばれる処理では、以下のような「手動によるディープコピーメソッド」を実装せよ。
‘ 手動によるディープコピーの例(パフォーマンスクリティカルな場面用)
Public Function CloneManually() As Customer
Dim clone As New Customer()
clone.Id = Me.Id
clone.Name = Me.Name
‘ 参照型の子オブジェクトの複製
If Me.ShippingAddress IsNot Nothing Then
clone.ShippingAddress = New Address() With {
.ZipCode = Me.ShippingAddress.ZipCode,
.Prefecture = Me.ShippingAddress.Prefecture,
.City = Me.ShippingAddress.City
}
End If
‘ コレクションのディープコピー(要素ごと nuevos インスタンス化)
If Me.OrderHistory IsNot Nothing Then
clone.OrderHistory = New List(Of Order)()
For Each order In Me.OrderHistory
clone.OrderHistory.Add(New Order() With {
.OrderId = order.OrderId,
.Amount = order.Amount,
.OrderDate = order.OrderDate
})
Next
End If
Return clone
End Function
—
チーフアーキテクトからの提言
「動けばいい」という妥協の産物は、必ずシステムの寿命を縮める。
参照型のコピーにおいて「たまたま動いている」状態は、将来の仕様変更やリファクタリング時に、原因不明のバグとして牙を剥く。
- 浅いコピー(シャローコピー)でデータが破壊されるリスクを常に意識する。
- 古い `ICloneable` は使わず、目的に応じて `System.Text.Json` によるシリアライズ、または堅牢な手動ディープコピーを使い分ける。
この原則をチーム全体で徹底し、絶対にバグを出さない強靭なVB.NETアプリケーションを構築してほしい。
