こんにちは!開発現場で日々、コードと格闘お疲れ様です。
今日は、VB.NETの中級者なら誰もが一度はハマる「オブジェクトのコピー」、特に深淵なる「ディープコピー(深い複製)」の世界についてお話しします。
「マクロの記録」や簡単な変数代入(`Dim b = a`)の感覚でオブジェクトを扱っていると、現場で突然、「片方を書き換えたら、もう片方まで変わってしまった!」という怪奇現象(バグ)に直面します。
ここをクリアすれば、VB.NETのメモリ管理や参照型の本質が手に取るように分かるようになりますよ。優しく、かつ骨太に解説していきますね!
—
1. なぜ「シャローコピー(浅いコピー)」は裏切るのか?
まずは、VB.NETにおける変数の振る舞いの基本を思い出しましょう。
クラス(Class)で作ったオブジェクトは「参照型」です。変数の箱の中に入っているのは、実体そのものではなく、「実体がどこにいるか」というメモ地址(アドレス)に過ぎません。
恐怖の「参照共有」
例えば、社員情報を表する `Employee` クラスの中に、所属部署を表す `Department` クラスがネスト(入れ子)されているとします。
‘ 社員クラス
Public Class Employee
Public Property Name As String
Public Property Dept As Department
End Class
‘ 部署クラス
Public Class Department
Public Property DeptName As String
End Class
ここで、A君のデータを作ってB君に代入し、B君の部署名だけを変えようとすると…?
Dim empA As New Employee With {.Name = “佐藤”, .Dept = New Department With {.DeptName = “開発部”}}
‘ 単純に代入してみる(シャローコピー)
Dim empB As New Employee()
empB.Name = empA.Name
empB.Dept = empA.Dept ‘ ← ここが地獄の入り口!
‘ B君の部署名を変えたつもりが…?
empB.Dept.DeptName = “人事部”
‘ 画面出力してみると
Console.WriteLine(empA.Dept.DeptName) ‘ なぜか「人事部」になってしまう!
【図解イメージ】
empA ──> [ Employee実体 ] ──> Dept ──┐
│ (同じ実体を共有!)
empB ──> [ Employee実体 ] ──> Dept ──┘
`empB.Dept` に代入されたのは、`empA.Dept` と全く同じメモリ上のオブジェクトへの参照です。だから、片方をいじると、もう片方も連動して変わってしまう。これが、意図しないバグ(副作用)の正体です。
—
2. 伝統的な「ICloneable」の限界と罠
.NETの世界には、オブジェクトを複製するためのインターフェースとして `ICloneable` が用意されています。
「じゃあ、これを使えばバッチリだね!」……と言いたいところですが、実は実務の現場では `ICloneable` はあまり推奨されません。
なぜ ICloneable は使えないのか?
1. 「浅い(Shallow)」か「深い(Deep)」かが実装者に委ねられている
インターフェースの定義には「Cloneメソッドを実装しなさい」としか書かれておらず、それがシャローコピーを返すのか、ディープコピーを返すのか、使ってみるまで分からないという設計上の欠陥があります。
2. 戻り値が `Object型` である
呼び出し側で必ず型変換(CType等)が必要になり、コードが冗長になります。
こうした歴史的経緯から、モダンなVB.NET開発では `ICloneable` を実装するのではなく、目的に特化した独自のクローンメソッドや仕組みを作るのがプロの常道です。
—
3. 実践!ネスト構造を完全に切り離す「ディープコピー」の実装
では、ネストした参照型(階層構造)であっても、完全に別のメモリ空間に複製を作る「ディープコピー」をVB.NETでどう実装するか。
最も確実で美しいアプローチの一つが、「コンストラクタや専用メソッドで再帰的に新しいインスタンスを組み立て直す方法」です。
以下のコードを見てください。実務でそのまま使える堅牢な実装です。
Imports System
Namespace DeepCopySample
‘ 部署クラス
Public Class Department
Public Property DeptName As String
‘ 既定のコンストラクタ
Public Sub New()
End Sub
‘ コピーコンストラクタ(自分自身を複製する専用の部屋)
Public Sub New(other As Department)
If other IsNot Nothing Then
Me.DeptName = other.DeptName
End Sub
End Sub
‘ 自身のクローンを返すメソッド
Public Function Clone() As Department
Return New Department(Me)
End Function
End Class
‘ 社員クラス
Public Class Employee
Public Property Name As String
Public Property Dept As Department
Public Sub New()
End Sub
‘ コピーコンストラクタ(ネストしたオブジェクトも連鎖的にクローンする!)
Public Sub New(other As Employee)
If other IsNot Nothing Then
Me.Name = other.Name
‘ Departmentも新しく実体を作り直して代入する(ここで参照が完全分離する)
If other.Dept IsNot Nothing Then
Me.Dept = other.Dept.Clone()
End If
End Sub
End Sub
Public Function Clone() As Employee
Return New Employee(Me)
End Function
End Class
End Namespace
このコードの美しさとポイント
- コピーコンストラクタの活用: 「自分と同じ型のオブジェクトを受け取り、そのプロパティを新しいメモリ上に移し替える」専用のコンストラクタを用意します。
- 連鎖的(再帰的)な複製: `Employee` が複製される際、内部の `Department` も自動的に `Clone()` 経由で新しくインスタンス化されます。これにより、どれだけ階層が深くても、参照が完全に独立します。
—
4. さらにスマートに!シリアライゼーションを使った裏技(注意点つき)
「オブジェクトのプロパティが数十個あって、わざわざコピーコンストラクタを書くのが面倒くさい!」という巨大なオブジェクト構造を扱う場合、JSONシリアライザを使って一瞬でディープコピーを作るテクニックもあります。
VB.NET(.NET Core / .NET 5以降)であれば、`System.Text.Json` を使うのがスマートです。
Imports System.Text.Json
Public Module ObjectExtensions
‘ 汎用的なディープコピー拡張メソッド
Public Function DeepCopy(Of T)(ByVal source As T) As T
‘ 1. 一度JSON文字列に完全にシリアライズ(文字化)する
Dim jsonString As String = JsonSerializer.Serialize(source)
‘ 2. それを全く別の新しいオブジェクトとしてデシリアライズ(復元)する
Return JsonSerializer.Deserialize(Of T)(jsonString)
End Function
End Module
使い方
これさえ定義しておけば、どんなに複雑なクラスであっても、一行でディープコピーが完了します。
Dim empA As New Employee With {.Name = “佐藤”, .Dept = New Department With {.DeptName = “開発部”}}
‘ 拡張メソッドで一発ディープコピー!
Dim empB As empA.DeepCopy()
empB.Dept.DeptName = “人事部”
‘ empA.Dept.DeptName は「開発部」のまま守られる!
⚠️ 注意点:
このJSON方式は非常に楽ですが、裏側でテキストへの変換・再構築を行うため、パフォーマンスがシビアな極限のループ処理などではボトルネックになる可能性があります。また、クラス内のイベントハンドラや IntPtr などの非マネージリソースはコピーできません。
ドメインモデルの規模やパフォーマンス要件に合わせて、手動のコピーコンストラクタと使い分けるのが「真のエエンジニア」の判断力です。
—
まとめ
- シャローコピー(単純代入)は、参照先のアドレスしかコピーしないため、意図せぬバグの温床になる。
- 伝統的な `ICloneable` は戻り値が `Object` であり、実装方針も曖昧なため、モダンな現場では避けたほうが無難。
- 確実なパフォーマンスと安全性を求めるなら「コピーコンストラクタによる再帰的複製」。
- 開発スピードやメンテナンス性を優先するなら「JSONシリアライザ等を利用した拡張メソッド」。
オブジェクトのライフサイクルとメモリの動きを意識できるようになると、VB.NETでのプログラミングは驚くほど楽しく、堅牢になります。
「ここをクリアすれば、VB.NETの基本はバッチリですよ!」――ぜひ、今日のコードをご自身のプロジェクトで試してみてくださいね。
