第27章:Prototype ②:C#らしく(record + with)🧾

ねらい 🎯
- 「テンプレ(原型)からコピーして、ちょっとだけ変更✨」を C#の
record + withで気持ちよく書けるようにするよ〜😺 - Prototypeのキモである “コピーの境界(浅い/深い)” を、まずは安全側で体験するよ⚠️
- Visual Studioで「
recordが裏で何を生成してるか」も、チラ見して納得するよ👀🔍
※ここは 言語機能(
record/with)の安定した仕様が中心だよ。細部の最新差分は、Visual StudioのF1(ドキュメント)で最終確認してね📚✨
到達目標 ✅
recordが「値っぽい比較(Equals)」をしてくれる理由を説明できる🙂withで「コピー+変更」を作れて、元が変わらないことをテストで確認できる🧪- 「
recordにListを入れると危ない😵」が体験できて、**安全な選択肢(Immutable系/コピー)**を選べる🛡️
手順 🧭
1) Prototypeをrecordでやると気持ちいい場面を選ぶ 🧩✨
Prototypeは「同じ初期形を量産する」パターンだよ🧬 C#で相性がいいのは、こういう“テンプレ/下書き”👇
- 注文の下書き
OrderDraft(まだID確定してない)🛒 - 画面入力の入力途中
FormDraft📝 - 設定テンプレ
EmailTemplate✉️
逆に、**IDで同一性が決まる“エンティティ”**は record だと事故りやすい(後で説明するね)⚠️
2) recordの超ざっくり感覚をつかむ 🧠🌸
recordは「データの中身が同じなら同じ扱い」に寄せた型だよ🙂
ふつうのclassは「同じ参照なら同じ扱い」になりがちだけど、recordは 中身でEquals してくれるのが大きい✨
record class:参照型(でもEqualsは中身寄り)record struct:値型(小さめの値オブジェクトに便利)
3) withは「コピーして、指定したところだけ差し替え」🔁✨
withはこういうイメージだよ👇
- 元をコピーする(複製)
{ ... }で書いたプロパティだけ上書きする
だから「テンプレから派生版を作る」がめっちゃ得意😺
4) まずは“安全寄り”の例:Immutableでコピー事故を減らす 🛡️🍀
List<T>みたいな可変コレクションは、withしても参照が共有されやすくて危ない😵
なのでここでは Immutable(不変) を使って安全にいくよ✨(定番の選択肢!)
using System;
using System.Collections.Immutable;
public enum NotificationPreference
{
None,
Email
}
public readonly record struct Money(decimal Amount)
{
public static Money Jpy(decimal amount) => new(amount);
}
public sealed record OrderLine(string Sku, int Quantity, Money UnitPrice);
public sealed record OrderDraft(
string CustomerEmail,
ImmutableArray<OrderLine> Lines,
Money ShippingFee,
NotificationPreference Notification
)
{
// 位置引数recordでも、こういう“コンパクトコンストラクタ”で不変条件チェックできるよ🛡️
public OrderDraft
{
if (string.IsNullOrWhiteSpace(CustomerEmail))
throw new ArgumentException("CustomerEmail is required.", nameof(CustomerEmail));
if (ShippingFee.Amount < 0)
throw new ArgumentOutOfRangeException(nameof(ShippingFee), "ShippingFee must be >= 0.");
}
}
✅ 使ってみよう(テンプレ→派生)✨
using System.Collections.Immutable;
var baseDraft = new OrderDraft(
CustomerEmail: "a@example.com",
Lines: ImmutableArray.Create(
new OrderLine("SKU-APPLE", 1, Money.Jpy(1200m))
),
ShippingFee: Money.Jpy(500m),
Notification: NotificationPreference.Email
);
// 送料だけ無料にした“派生版”を作る🎁
var freeShippingDraft = baseDraft with
{
ShippingFee = Money.Jpy(0m)
};
// 行を追加した“派生版”を作る🍎➕🍌
var addOneMoreDraft = baseDraft with
{
Lines = baseDraft.Lines.Add(new OrderLine("SKU-BANANA", 2, Money.Jpy(300m)))
};
ポイント💡
baseDraftはそのまま残る(元は変わらない)🙂withは「派生版を作る」感覚で使える✨
5) Visual Studioで「recordが生成したもの」をチラ見する👀🔍
“読む”と理解が一段深くなるよ〜📚✨
recordの型名にカーソル置いて F12(定義へ移動)- Object Browser や IntelliSense で生成メンバーを眺める
- 「
Equals」「GetHashCode」「(型によっては)Deconstruct」あたりが見えるはず🙂
ここで狙うのは「えっ、recordって勝手に色々用意してくれるんだ!」の納得感だよ😺✨
6) “浅いコピー事故”をわざと起こしてみる(超大事)⚠️🧪
withは万能な“深いコピー機”ではないよ😵
可変な参照型メンバー(List<T>など)は共有されやすい!
まずは危ない例👇
using System.Collections.Generic;
public sealed record BadDraft(List<string> Tags);
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
[TestClass]
public class PrototypeWithRecordTests
{
[TestMethod]
public void With_shares_reference_members_when_they_are_mutable()
{
var original = new BadDraft(new List<string> { "A" });
var copy = original with { }; // 見た目はコピーだけど…👀
copy.Tags.Add("B"); // copyをいじったつもり😇
// originalも増えてしまう😵(参照が共有されてるから)
CollectionAssert.AreEqual(new[] { "A", "B" }, original.Tags);
}
}
✅ 対策は2つ(定番)🛡️✨
- (A) Immutable系にする(おすすめ!)
- (B) コピーして渡す(
ToList()など)
さっきのImmutableArrayの例は(A)だよ🙂
よくある落とし穴 🕳️⚠️
-
recordを「IDで同一性が決まるエンティティ」に使って、Equalsが期待とズレる😵- 例:
Order(注文)そのものをrecordにすると「中身が同じなら同一扱い」になりがちで危険 - 代わりに、テンプレ/下書き/値オブジェクトに寄せるのが安全🙂
- 例:
-
with=深いコピーだと思い込む(特にListで爆発)💥 -
record structに“でかいデータ”を入れて、コピーコストが増える📦💦- 値型はコピーが起きやすいから、小さめの値(
Moneyとか)向きだよ🙂
- 値型はコピーが起きやすいから、小さめの値(
-
MemberwiseCloneに手を出して「コピーできた気」になる(でも浅い)😇- ここは注意喚起だけ!本格的には次章で⚠️
ミニ演習(10〜30分)✍️🌼
演習1:withで派生版を3つ作る🎨✨
-
baseDraftから- 送料0円
- 通知OFF
- 行を1つ追加 の3パターンを作ってみてね🙂
チェック用に「元が変わってない」テストも書こう🧪✨
[TestMethod]
public void With_creates_variants_without_mutating_original()
{
var baseDraft = new OrderDraft(
CustomerEmail: "a@example.com",
Lines: ImmutableArray.Create(new OrderLine("SKU-APPLE", 1, Money.Jpy(1200m))),
ShippingFee: Money.Jpy(500m),
Notification: NotificationPreference.Email
);
var variant = baseDraft with { ShippingFee = Money.Jpy(0m) };
Assert.AreEqual(Money.Jpy(500m), baseDraft.ShippingFee);
Assert.AreEqual(Money.Jpy(0m), variant.ShippingFee);
}
演習2:わざとListで事故らせて、Immutableで直す⚠️➡️🛡️
BadDraftのテストを通して「なるほど…😵」を体験TagsをImmutableArray<string>(またはImmutableList<string>)に置き換えて、同じ事故が起きない形に修正✨
演習3:便利メソッドを“汎用化しすぎず”に1個だけ作る🔧🙂
OrderDraftに「行を追加した派生を返す」だけのメソッドを追加してみてね👇
public sealed record OrderDraft(/* 省略 */)
{
public OrderDraft AddLine(OrderLine line) =>
this with { Lines = Lines.Add(line) };
}
自己チェック ✅🧠
recordがclassと違うのは「何でEqualsするか」を説明できる?🙂withが作るのは 新しいインスタンスで、元は基本変わらない…って言える?🔁✨List<T>をrecordに入れると危ない理由を、テストで見せられる?⚠️🧪