CodeGym /コース /C# SELF /イミュータビリティと with

イミュータビリティと with

C# SELF
レベル 19 , レッスン 2
使用可能

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)

ポジショナルrecordのプロパティは読み取り専用(init-only)として宣言される。作成後に変更できないけど、with式を使えば、変更したプロパティで新しいコピーを作れるよ。

ミュータブルなクラス vs イミュータブルなrecord

クラス ポジショナルRecord
デフォルトのプロパティ
get; set;
イミュータブル
get; init;
どうやって変更する?
プロパティにアクセス
新しいコピーを作るだけ
オブジェクトの比較
参照で(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のやつ)だけを使うのがベスト。

本物のイミュータビリティを保証したいなら、こういうコレクションを使うか、自分でディープコピーしよう!

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION