第29章:Prototype ④:浅い/深いコピー注意⚠️

ねらい 🎯✨
Prototype(コピーで量産)は便利だけど、**「コピーしたのに中身がつながってた😱」**が起きやすい! この章では 浅いコピー(shallow)/ 深いコピー(deep) を体で理解して、事故らない使い方を身につけるよ🫶💕
到達目標 🏁🌸
-
✅ 「浅いコピー」と「深いコピー」を、例を出して説明できる
-
✅
record + withが 参照型プロパティは共有しやすい理由がわかる -
✅ 「安全なコピー戦略」を3パターン言える
- 不変(immutable)で浅いコピーでも安全にする
- “箱(List/Dictionary)だけ”作り直して共有を断つ
- 必要な範囲だけ深いコピーする(やりすぎない)
-
✅ テストで「つながってる事故」を再現し、直せる🧪✨
手順 🧭🧪✨
1) まず「浅いコピー事故」をわざと起こす 😈💥
record + with は超便利だけど、**List/Dictionary みたいな参照型は“同じインスタンスを指しがち”**だよ⚠️
using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
public readonly record struct Money(decimal Amount, string Currency);
public sealed record OrderLine(string Sku, int Quantity, Money UnitPrice);
// ⚠️ List と Dictionary は “参照型で可変” だから要注意!
public sealed record OrderTemplate(
Guid TemplateId,
string Name,
List<OrderLine> Lines,
Dictionary<string, string> Meta
);
[TestClass]
public class PrototypeCopyPitfallsTests
{
[TestMethod]
public void With_copy_is_shallow_for_reference_properties()
{
var t1 = new OrderTemplate(
Guid.NewGuid(),
"Starter",
new List<OrderLine>
{
new("SKU-001", 1, new Money(1200, "JPY")),
},
new Dictionary<string, string>
{
["note"] = "gift"
}
);
var t2 = t1 with { Name = "Starter Copy" };
// ここがポイント:参照が同じ=つながってる😱
Assert.IsTrue(ReferenceEquals(t1.Lines, t2.Lines));
Assert.IsTrue(ReferenceEquals(t1.Meta, t2.Meta));
// t2だけに追加したつもりが…
t2.Lines.Add(new OrderLine("SKU-999", 1, new Money(500, "JPY")));
t2.Meta["note"] = "changed";
// t1まで変わってる!💀
Assert.AreEqual(2, t1.Lines.Count);
Assert.AreEqual("changed", t1.Meta["note"]);
}
}
✅ ここまでで「コピーしたのに元が変わる」感覚が掴めたらOK🙆♀️✨
2) 事故の原因を言葉にする 🧠📝
-
withは「オブジェクトを複製」するけど 参照型プロパティ(List/Dictionary/配列/自作classなど)は参照をコピーしやすい -
つまり
- 箱(List)が同じ → 片方で
Addすると両方に見える - 中身(要素)が可変 → 片方で要素を書き換えると両方に影響
- 箱(List)が同じ → 片方で
3) 対策①:不変(immutable)に寄せて「浅いコピーでも安全」にする 🛡️🌟
いちばん強い方針はこれ💪 中身が変わらないなら、共有してても問題になりにくいよ✨
record(特にinit)で「作ったら基本変更しない」設計にする- コレクションも「変更できない形」に寄せる(後述)
💡学習のコツ: Prototypeは「コピーの便利さ」よりも、**“コピー後に安全に使える形”**を作るのが本体だよ🧡
4) 対策②:箱(List/Dictionary)だけ作り直して参照共有を切る ✂️📦
「要素は不変(record)だから共有でもOK、でも箱は別にしたい」ってときはこれが最小で効くよ🙂✨
public static class OrderTemplateCopies
{
public static OrderTemplate CopyForEditing(OrderTemplate source)
=> source with
{
Lines = new List<OrderLine>(source.Lines),
Meta = new Dictionary<string, string>(source.Meta)
};
}
[TestClass]
public class PrototypeCopyFixesTests
{
[TestMethod]
public void CopyForEditing_breaks_reference_sharing_of_containers()
{
var t1 = new OrderTemplate(
Guid.NewGuid(),
"Starter",
new List<OrderLine> { new("SKU-001", 1, new Money(1200, "JPY")) },
new Dictionary<string, string> { ["note"] = "gift" }
);
var t2 = OrderTemplateCopies.CopyForEditing(t1);
Assert.IsFalse(ReferenceEquals(t1.Lines, t2.Lines));
Assert.IsFalse(ReferenceEquals(t1.Meta, t2.Meta));
t2.Lines.Add(new OrderLine("SKU-999", 1, new Money(500, "JPY")));
t2.Meta["note"] = "changed";
Assert.AreEqual(1, t1.Lines.Count);
Assert.AreEqual("gift", t1.Meta["note"]);
}
}
✅ これで「箱の共有」が切れて、事故がかなり減るよ🎉
ただし注意⚠️:要素が class で可変なら、要素の共有でまだ事故る可能性あり!
5) 対策③:必要な範囲だけ “深いコピー” する 🐳🔁
深いコピーは万能に見えるけど、やりすぎると 重い・複雑・壊れやすい😵💫 だから “業務的に必要な範囲だけ” 深くするのがコツだよ✨
例:OrderLine が可変(class)なら、Linesの中身も複製する…みたいにね👇
public sealed class MutableOrderLine
{
public string Sku { get; set; } = "";
public int Quantity { get; set; }
}
public sealed record MutableTemplate(List<MutableOrderLine> Lines)
{
public MutableTemplate DeepCopy()
=> new(new List<MutableOrderLine>(
Lines.ConvertAll(x => new MutableOrderLine { Sku = x.Sku, Quantity = x.Quantity })
));
}
ポイント💡
- 「何を共有してよくて」「何を共有しちゃダメか」を決めるのが設計✨
- 深いコピーは 設計判断そのもの(自動化しにくい)だよ🧠
6) MemberwiseClone の扱い(名前は有名、でも慎重に)⚠️🧯
MemberwiseClone は 浅いコピーだよ(参照は共有)
しかも protected だから「クラス内部でしか呼べない」系のやつ🙂
- ✅ 仕組み理解として知るのはOK
- ⚠️ “とりあえず MemberwiseClone” は事故の香りがするので避けがち💦
7) デバッガで「つながり」を目で見る 👀🔗
Visual Studio のデバッグで、ここを見ると一発で理解できるよ✨
ReferenceEquals(t1.Lines, t2.Lines)を Watcht1.Linesとt2.Linesの中身が同時に変わるか観察- Dictionaryも同様にチェック🔍
8) AI補助を使うなら(雛形はOK、判断は人間)🤖🧡
AIに頼むときは、**「どこまで深くコピーするか」**を必ず指定してね⚠️
📝プロンプト例(そのまま使えるよ✨)
- 「
record + withを使い、List/Dictionaryは新しいインスタンスにしてください」 - 「要素は
recordで不変なので、要素の複製は不要です」 - 「汎用のDeepCloneユーティリティは作らず、この型だけで完結してください」
👀レビュー観点
- 参照型プロパティを
new List<>(...)/new Dictionary<>(...)で切ってる? - “なんでもDeepCopyする汎用ツール”を生やしてない?(禁止ゾーン🚫)
- テストで「共有してない」を確認してる?
よくある落とし穴 🕳️🐾
- ❌
withしたから安全だと思い込む(List/Dictionaryで即死😇) - ❌ 「深いコピー=正義」になって重くなる(性能・複雑度が爆増)
- ❌
ICloneableで逃げる(浅いの?深いの?契約が曖昧になりがち) - ❌ 共有していい範囲を決めずにコピーする(後から地雷💣)
- ⚠️ 注意:このページはWeb検索での追加確認ができない状態のため、仕様が安定している範囲(
record/with、参照共有の基本原理)に絞って解説しています。最新の推奨や細部は公式ドキュメントも合わせて確認してね🙏📚
演習(10〜30分)🏃♀️💨🧪
演習A:事故再現 → 修正
- 上の「浅いコピー事故」テストを通す(事故を確認)
CopyForEditingを作って 箱の共有を切る- テストを追加:
ReferenceEqualsが false になることを確認✨
演習B:共有していい/ダメを言語化
- 「共有していいもの」3つ書く(例:Money、SKU文字列、固定の設定値)
- 「共有しちゃダメなもの」3つ書く(例:List、Dictionary、状態を持つオブジェクト)
演習C:コピー戦略メモ(1行)
- 例:「このTemplateはLines/Metaの箱はコピー、要素は不変だから共有」
自己チェック ✅💖
- ✅
withが参照型プロパティを共有しうる理由を説明できた? - ✅ 「箱だけコピー」「必要な範囲だけ深いコピー」「不変化」のどれを選んだか言えた?
- ✅ テストで
ReferenceEqualsを使って“つながり”を検証できた? - ✅ 余計な汎用DeepCloneを作らず、型の責務としてコピーを置けた?