CodeGym /課程 /C# SELF /record、class 和 struct 的差異

record、class 和 struct 的差異

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

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
2
任務
C# SELF, 等級 19, 課堂 4
上鎖
建立 record 並比較物件
建立 record 並比較物件
2
任務
C# SELF, 等級 19, 課堂 4
上鎖
使用 `with` 運算子來寫入資料
使用 `with` 運算子來寫入資料
1
問卷/小測驗
DTO的問題,等級 19,課堂 4
未開放
DTO的問題
DTO、record 跟 with
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION