1. イミュータビリティって何?
まずは例え話から始めよう:君が毎日売上レポートを作る経理担当だと想像してみて。プロっぽく、先週のレポートを直接書き換えたりせず、古いレポートを元に新しいレポートを作るよね。プログラミングのイミュータブルなオブジェクトも同じで、一度作ったらもう変更できない。「変更」したい時は新しいコピーを作るってこと。
イミュータビリティ(immutability)は、オブジェクトが初期化後に変わらない性質のこと。全部のプロパティが「凍結」される感じ。もし違う値が欲しいなら、新しいオブジェクトを作るしかないんだ。
なんで必要なの?
- 安全性が高まる:誰も君のオブジェクトをうっかり変更できないから、データが壊れる心配がない。特にマルチスレッドなプログラムだと、複数のスレッドが同時に何かを変えようとしてバグることが多い。
- デバッグが楽:オブジェクトが変わらないから、作成後に何が起きたかハッキリ分かる。
- データの受け渡しが便利。特に分散システムだと、コピーがバラバラになりがちだからね。
- 「スナップショット」(snapshots)が簡単に作れる。変更履歴も分かりやすい!
2. record型のイミュータビリティ
普通のclassを宣言すると、プロパティはデフォルトでミュータブル(mutable)だよ。例:
public class UserProfile
{
public string Name { get; set; }
public int Age { get; set; }
}
var user = new UserProfile { Name = "イワン", Age = 25 };
user.Age = 26; // 全然OK — 普通のクラスだから、年齢をその場で変えられる
recordの場合は逆で、イミュータブルなデータを保存するためのものなんだ。
public record UserProfile(string Name, int Age);
// オブジェクトを作成:
var user = new UserProfile("イワン", 25);
// 年齢を変えようとすると:
user.Age = 26; // コンパイルエラー:プロパティは読み取り専用!
ポジショナルrecordのプロパティは読み取り専用(init-only)として宣言される。作成後に変更できないけど、with式を使えば、変更したプロパティで新しいコピーを作れるよ。
ミュータブルなクラス vs イミュータブルなrecord
| クラス | ポジショナルRecord |
|---|---|
デフォルトのプロパティ |
イミュータブル |
| どうやって変更する? プロパティにアクセス |
新しいコピーを作るだけ |
| オブジェクトの比較 参照で(ReferenceEquals) |
値で(Equals) |
| データの受け渡しに便利? いつもじゃない |
うん |
3. with式
「recordって便利だけど、変更できないならどうすればいいの?」って思った?そこでwith式の魔法が登場!
withは、新しいコピーのrecordを作るための特別な構文。
つまり「このオブジェクトをコピーして、ここだけちょっと変えたい」って時に使うんだ。
超シンプルな例
var user1 = new UserProfile("アンナ", 30);
// ...でも人生は進む、アンナも年を取る
var user2 = user1 with { Age = 31 };
// user1はそのまま、user2は1歳年上のコピー
Console.WriteLine(user1); // UserProfile { Name = アンナ, Age = 30 }
Console.WriteLine(user2); // UserProfile { Name = アンナ, Age = 31 }
裏側では?
これはミュータントクローンじゃなくて、新しいオブジェクトなんだ。自動生成されるClone()メソッドでコピーを作って、新しい値をセットしてる。
もしwith式が現実にあったら、朝起きるたびに「疲れた体」じゃなくて、気分も筋肉も調整済みの自分のコピーでスタートできる(recordだったらね)。
4. ネストとコピーについてちょっとだけ
recordの中に他のrecordがある場合は問題なし:
public record Address(string City, string Street);
public record Student(string Name, int Age, string Email, Address Home);
var a1 = new Address("モスクワ", "トヴェルスカヤ");
var s1 = new Student("レナ", 21, "lena@mail.ru", a1);
var s2 = s1 with { Home = a1 with { Street = "アルバート" } };
ここでは本当にイミュータブルに動くよ。なぜならネストしたAddressもrecordだから。
5. 最後の注意点
ポジショナルrecord = コンパクト
recordは「短い」書き方(ポジショナル構文)で宣言できる。この場合、全部のプロパティが自動的にinit-onlyになるよ。
public record Course(string Name, int Credits);
var c1 = new Course("C#", 5);
var c2 = c1 with { Credits = 6 };
読み取り専用プロパティ(init-only)とのアナロジー
recordではこんな風にプロパティを明示的に宣言できる:
public record Student
{
public string Name { get; init; }
public int Age { get; init; }
}
こういうプロパティも初期化時(またはwithで)しか変更できないよ。
6. 実践:デモアプリ
じゃあ、学習用の「オンラインスクール」を作ってみよう。すでに学生用のrecordがあるとするね:
public record Student(string Name, int Age, string Email);
あるある:誰かがメールアドレスを間違えたままアカウントを作っちゃった。どうやってemailを「更新」する?もちろんwithで!
var student = new Student("エカテリーナ", 19, "kate@school.com");
var updatedStudent = student with { Email = "ekaterina@school.com" };
// オブジェクトを確認:
Console.WriteLine(student); // Student { Name = エカテリーナ, Age = 19, Email = kate@school.com }
Console.WriteLine(updatedStudent); // Student { Name = エカテリーナ, Age = 19, Email = ekaterina@school.com }
7. よくあるミスと落とし穴
ここからはちょっと痛い話 — 学生がイミュータブルなrecordでよくやるミスについて。
- まず、「withは元のオブジェクトを変更する」と思い込む人が多い。でも実際は、元のオブジェクトはそのままで、新しいフィールドで新しいオブジェクトが作られる。これで新しい値を見失うこともあるから注意!
- それから、recordの中にミュータブルなオブジェクト(例えば配列やList)がある場合、with式はディープコピーしない!両方のコピーで同じコレクションを共有しちゃう。
public record Student(string Name, int Age, List<string> Subjects);
var s1 = new Student("オレグ", 22, new List<string> { "Math", "Physics" });
var s2 = s1 with { };
s1.Subjects.Add("C#"); // おっと、s2.Subjectsにも"C#"が入っちゃう
だから本当にイミュータブルな状態を保ちたいなら、プリミティブ型や自分自身がイミュータブルなコレクション(ImmutableList<T>とか、System.Collections.Immutableのやつ)だけを使うのがベスト。
本物のイミュータビリティを保証したいなら、こういうコレクションを使うか、自分でディープコピーしよう!
GO TO FULL VERSION