1. 為什麼會需要 record
在 C#(還有整個 .NET)裡,class(class)跟 struct(struct)一直是主要的建構塊。但它們各自都有點不夠用。class 是參考型別、可變,而且預設是用參考比較(很少例外)。struct 是值型別(傳遞時會複製),預設是用 byte-by-byte 比較,而且通常也是可變的(直到有了 readonly struct)。
如果你想要一個簡單又直觀的資料記錄方式,方便用值來比較、快速複製、又不用擔心物件被誰改掉——以前都得自己寫一堆 code 或用像 ValueTuple 或 System.Tuple 這種 library。但這些都沒那麼漂亮、沒那麼直觀。
所以 C# 9 才加了 record——一種結合了簡潔宣告、不變性安全、還有值比較行為的資料型別。
2. 四種型別一次看懂
| class | struct | record | record struct | |
|---|---|---|---|---|
| 分類 | 參考型別 | 值型別 | 參考型別 | 值型別 |
| 可變性 | 預設可變 | 預設可變 | 預設不可變(init) | 預設不可變(init) |
| 比較方式 | 用參考(==) | 用值(欄位) | 用值(fields/properties) | 用值(fields) |
| 複製 | 只能手動 | 只能手動 | 有內建支援(with) | 有內建支援(with) |
| 繼承 | 可以 | 不行 | 可以 | 不行 |
| 不變性 | 要自己實作 | 要自己實作 | 超簡單實作 | 超簡單實作 |
| 語法 | 最長 | 短 | 最短(位置型) | 滿短的 |
| 在集合裡的行為 | 用參考 | 複製 | 用參考 | 複製 |
視覺化圖解
+----------------+ +----------------+ +--------------------+
| class | | struct | | record |
+----------------+ +----------------+ +--------------------+
| Reference Type | | Value Type | | Reference Type |
| Mutable | | Mutable | | Immutable (init) |
| == : Reference | | == : By Fields | | == : By Value |
+----------------+ +----------------+ +--------------------+
3. record、class、struct 的本質差異
記憶體行為:參考還是值?
- class 跟 record 都是參考型別。傳進 function 時,複製的是物件的參考。
- struct 跟 record struct 是值型別。它們永遠是 byte-by-byte 複製(除非你明確用參考傳遞)。
class PointClass { public int X; public int Y; }
struct PointStruct { public int X; public int Y; }
record PointRecord(int X, int Y);
record struct PointRecordStruct(int X, int Y);
void Demo()
{
var pc = new PointClass { X = 1, Y = 2 };
var ps = new PointStruct { X = 1, Y = 2 };
var pr = new PointRecord(1, 2);
var prs = new PointRecordStruct(1, 2);
ChangeY(pc); // pc.Y 會被改!
ChangeY(ps); // ps.Y 不會變——複製!
ChangeY(pr); // pr.Y 會被改!
ChangeY(prs); // prs.Y 不會變——複製!
}
void ChangeY(dynamic p) { p.Y = 99; }
如果你覺得 dynamic 這裡很神奇——對啦,這只是為了 demo,讓你看到 struct 的值不會變,但 class 或 record(參考型)會變。
比較:怎麼知道兩個物件一樣?
- class:預設用參考比較(== 只有同一個記憶體物件才 true),除非你 override Equals。
- struct:預設用所有欄位的值來比較。
- record:用主建構子的所有欄位/屬性值來比較。
class Foo { public int A; public int B; }
record Bar(int A, int B);
var foo1 = new Foo { A = 42, B = 1 };
var foo2 = new Foo { A = 42, B = 1 };
var bar1 = new Bar(42, 1);
var bar2 = new Bar(42, 1);
Console.WriteLine(foo1 == foo2); // False! 不同物件
Console.WriteLine(bar1 == bar2); // True! 值一樣
有趣小知識:
record struct 更酷:它們也是「值比較」,像一般 struct,但有 record 的語法跟功能。
4. 不變性:誰能保證不會被改?
來比較一下物件的安全性:
- class:預設很容易被改,除非你把所有欄位都設 readonly。
- struct:一樣,但你可以宣告成 readonly struct,這樣欄位跟屬性都不能改。
- record:通常欄位都用 init 修飾,也就是只能在建構子或物件初始化器(with)裡設值。這樣傳資料很安全。
- record struct:一樣,你可以用 readonly record struct,就能有值型別的不變性,還有 record 的好處。
record Person(string Name, int Age);
var p1 = new Person("阿列克謝", 23);
// p1.Age = 24; // 錯誤!只能 init
var p2 = p1 with { Age = 24 }; // OK!會產生一個新物件,資料有變
如果你有一個大專案,裡面一堆 entity 物件,record 可以幫你避免很多「誰改了欄位結果全炸」的 bug。
5. 語法:宣告長怎樣?怎麼不搞混?
// class
public class Product
{
public int Id { get; init; }
public string Name { get; init; }
}
// struct
public struct Point
{
public int X { get; set; }
public int Y { get; set; }
}
// record
public record Product(int Id, string Name);
// record struct
public record struct Point(int X, int Y);
你看,record 型別的寫法最短。建構子、解構、等值、ToString() 等等都直接幫你做好。
| 語法 | 自動獲得的功能 | 能不能繼承 |
|---|---|---|
| class | 幾乎沒自動 | 可以 |
| struct | 幾乎沒自動 | 不行 |
| record | ToString, Equals, Deconstruct, with | 可以 |
| record struct | ToString, Equals, Deconstruct, with | 不行 |
6. 複製跟 with 運算子
只有 record 跟 record struct 有 with 運算子,可以輕鬆複製物件並改部分屬性。
record User(string Name, int Age);
var user1 = new User("伊琳娜", 28);
var user2 = user1 with { Age = 29 }; // user2.Name == "伊琳娜", user2.Age == 29
class(class)就得自己寫複製方法,忘了複製某個欄位就等著出 bug 吧。
7. 繼承:誰能繼承誰
- class:支援標準繼承(class 階層、虛擬方法、抽象化等等)。
- struct:不支援(只能實作 interface)。
- record:支援繼承,但有限制(只能 record 跟 record 繼承,不能跟 class 混)。
- record struct:不支援,跟一般 struct 一樣。
record Animal(string Name);
record Dog(string Name, string Breed) : Animal(Name); // OK!
class Vehicle { }
class Car : Vehicle { } // OK!
// struct 不能繼承 struct
繼承的細節下個等級再說 :P
8. 各型別實際用在哪
- class:大型物件、有複雜行為、生命週期長、狀態會變、有階層(像是商業邏輯、UI 元件)。
- struct:小型值物件,需要超快複製、沒 GC、最小 overhead(像座標、顏色、金額——這些很容易複製的東西)。
- record:DTO(資料傳輸物件)、參數物件、設定參數、不變狀態、計算結果,這些很適合用內容來比較的東西。
- record struct:值型別、需要不變性、想要 record 行為但又不想在 heap 配置。
9. 常見錯誤跟陷阱
有時候工程師會以為 record 就是「class 的替代品」。其實不是!如果你寫的物件會在生命週期內改變狀態——用 class。
如果你要用參考來比較物件(像 singleton pattern 或物件生命週期很重要時)——用 class。
如果你要做像數字或座標點那種值物件——struct 或 record struct。
如果你要用不變、好比較、常常在應用層之間傳遞、放在集合、log、序列化的物件——就選 record。
還有要記得:如果你宣告了只有唯讀屬性的 record class,但忘了裡面的物件也是可變的——那些欄位還是會被改掉,只要它們本身是可變的。
record Student(string Name, List<int> Grades);
var s1 = new Student("安東", new List<int>() {5,5,5});
var s2 = s1 with { };
s2.Grades.Add(2); // 兩個物件都「中獎」了!s1.Grades == s2.Grades
GO TO FULL VERSION