1. 什麼是不可變性?
先來個比喻:想像你是個會計,每天都要做銷售報表。專業的你不會去改上週的報表,而是根據舊的做一份新的、資料更新的。程式裡的不可變物件也是這樣:一旦創建就不能再改,所謂的「改變」其實是創建一個新副本。
不可變性(immutability)就是物件初始化後就不能再變。它的所有屬性都「凍結」了:想要新值?那就創一個新物件吧。
這樣做有啥好處?
- 安全性高:沒人能不小心改掉你的物件,資料就不會突然壞掉。這在多執行緒程式裡超常見,大家同時改資料很容易出事。
- debug 超方便:物件不會變,你就很清楚它創建後發生了什麼。
- 資料傳遞很方便,特別是在分散式系統裡,副本可能會分開跑。
- 可以做「狀態快照」(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; // 沒問題——一般 class:年齡隨便改
record 預設就不一樣:它們就是拿來存不可變資料的。
public record UserProfile(string Name, int Age);
// 創建物件:
var user = new UserProfile("伊萬", 25);
// 嘗試改年齡:
user.Age = 26; // 編譯錯誤:屬性只能讀取!
位置型 record 的屬性預設只能讀(init-only)。你不能創建後再改,但可以用 with 表達式創建帶新屬性的副本。
可變 class vs 不可變 record
| Class | Record(位置型) |
|---|---|
屬性預設 |
不可變 |
| 怎麼改 直接改屬性 |
只能創新副本 |
| 物件比較 比參考(ReferenceEquals) |
比值(Equals) |
| 資料傳遞方便嗎 不一定 |
可以 |
3. with 表達式
你可能會想——「record 很酷,但不能改怎麼活?」這時 with 表達式就出場啦!
with 是一種語法糖,可以創建 record 的新副本,只改你想改的屬性。
也就是:「拿這個物件,複製一份,然後這幾個屬性換一下。」
最簡單的例子
var user1 = new UserProfile("安娜", 30);
// ...人生不斷前進,安娜又長大了
var user2 = user1 with { Age = 31 };
// user1 還是原本那個,user2 是副本但大一歲
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. 實戰:demo 小應用
來寫個我們的「線上學校」吧。假設我們已經有一個 學生 的 record:
public record Student(string Name, int Age, string Email);
經典場景:有人填錯 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#"
所以如果你想要真正的 immutable state,最好只用簡單型別或本身就不可變的集合(像 ImmutableList<T>,還有 System.Collections.Immutable)。
想要保證真的不可變,就用這些集合,或自己手動做深層複製吧。
GO TO FULL VERSION