CodeGym /課程 /C# SELF /不可變性跟 with 表達式

不可變性跟 with 表達式

C# SELF
等級 19 , 課堂 2
開放

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)

位置型 record 的屬性預設只能讀(init-only)。你不能創建後再改,但可以用 with 表達式創建帶新屬性的副本。

可變 class vs 不可變 record

Class 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 是副本但大一歲

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)。

想要保證真的不可變,就用這些集合,或自己手動做深層複製吧。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION