1. 資料傳遞的問題
假設我們有個學校的應用程式,要在不同模組之間傳遞學生資訊:名字、出生年、班級。通常我們會怎麼做?
我們會寫個這樣的 class:
public class Student
{
public string Name { get; set; }
public int YearOfBirth { get; set; }
public string Class { get; set; }
}
看起來很正常,但這種寫法有幾個問題:
- 如果要比較兩個學生是不是一樣,預設只會比物件參考。也就是說兩個欄位一樣但是不同物件的學生,還是不一樣!
- class 建立後還可以被改,這有時候會出錯(尤其物件已經被別的地方用到);
- 一堆「樣板」程式碼:建構子、比較方法、複製(clone)方法、ToString。
你大概已經猜到了:C# 可以幫我們解決這些麻煩!來認識一下 record 吧。
2. 什麼是 record?
record 是 C# 裡專門為資料儲存設計的一種型別。record 有兩個超重要的特點:
- 不可變性(immutability): record 物件預設是不可變的,也就是說屬性值只能在建立時設定一次,之後就不能改(其實 set 是 private)。雖然你也可以宣告可變的 record,但預設就是不可變。
- 值比較: 如果兩個 record 物件所有欄位值都一樣,它們就被視為相等(== 跟 .Equals() 會不一樣喔!)。
其實 record 超適合拿來在應用程式不同層之間傳資料(像是從資料庫到 controller,從 controller 到 view 之類的)。
3. record 的語法
最簡單的方式 — 位置語法
如果只是要傳一組值,可以用一行就宣告型別:
public record Student(string Name, int YearOfBirth, string Class);
底層發生什麼事?編譯器會自動幫你產生:
- 只有 getter 的自動屬性(private setter);
- 一個接收所有參數的建構子;
- 比較跟複製的方法;
- 很酷的 ToString,格式化輸出超方便!
怎麼用位置 record
來試試在我們的學校 app 裡用這個新型別:
var student1 = new Student("伊凡", 2008, "8A");
var student2 = new Student("瑪麗亞", 2008, "8B");
屬性存取跟平常一樣(只是不能改):
Console.WriteLine($"{student1.Name}, {student1.YearOfBirth}, {student1.Class}");
試著在建立後改屬性
student1.Name = "彼得"; // 錯誤!屬性是唯讀的。
如果你把這行註解拿掉,編譯器馬上會抱怨:不能設定唯讀屬性的值。
自動產生的 ToString 長這樣
Console.WriteLine(student1); // 會輸出:Student { Name = 伊凡, YearOfBirth = 2008, Class = 8A }
不用自己格式化,超美又一目了然!
4. record 物件的比較
提醒一下:如果用傳統 class 建兩個內容一樣的物件,它們還是不相等:
var a = new Student("伊凡", 2008, "8A");
var b = new Student("伊凡", 2008, "8A");
Console.WriteLine(a == b); // class:false
但如果 Student 是 record,等號就會像你想的一樣:
public record Student(string Name, int YearOfBirth, string Class);
var a = new Student("伊凡", 2008, "8A");
var b = new Student("伊凡", 2008, "8A");
Console.WriteLine(a == b); // record:true!
也就是說,兩個欄位一樣的 record 就算是不同記憶體物件,也會被當成一樣。
5. 裡面到底長怎樣
學生們常常會驚訝,原來編譯器幫我們做了這麼多事。來比較一下,如果用 class 要自己寫多少,record 又幫你省了多少。
手寫的傳統 class
public class Student
{
public string Name { get; }
public int YearOfBirth { get; }
public string Class { get; }
public Student(string name, int yearOfBirth, string @class)
{
Name = name;
YearOfBirth = yearOfBirth;
Class = @class; // 指向自己的 class
}
public override bool Equals(object? obj)
{
if (obj is not Student other) return false;
return Name == other.Name && YearOfBirth == other.YearOfBirth && Class == other.Class;
}
public override int GetHashCode()
{
return HashCode.Combine(Name, YearOfBirth, Class);
}
public override string ToString()
{
return $"Student {{ Name = {Name}, YearOfBirth = {YearOfBirth}, Class = {Class} }}";
}
}
難怪工程師都變神經質——同樣的東西要寫十遍!
record — 一行搞定
public record Student(string Name, int YearOfBirth, string Class);
6. record 跟不可變性:哪些能做,哪些不能
record 預設屬性都是唯讀,這可以避免超多 bug。但如果你真的很想(像是要對很舊的 API),也可以宣告可變的 record:
public record MutableStudent
{
public string Name { get; set; }
public int YearOfBirth { get; set; }
public string Class { get; set; }
}
這樣欄位就能改了,但你會失去一些優點(像安全性)。
7. record 的解構
因為位置語法很像 tuple,所以 record 也能很簡單解構:
var student = new Student("伊凡", 2008, "8A");
var (name, year, className) = student;
Console.WriteLine($"{name} - {year}, {className}"); // 伊凡 - 2008, 8A
編譯器會自動幫每個位置 record 產生 Deconstruct 方法,這對 LINQ、switch pattern 超方便,整個人生都美好了。
8. record 是 class 還是 struct?
預設 record 是參考型別,跟 class 一樣。也就是說所有參考型別的特性(堆積儲存、複製參考等等)都一樣。
如果你想要值型別(value type),C# 也有對應的語法,可以這樣寫:
public record struct Point(int X, int Y);
但大部分傳資料還是用經典的 record,也就是參考型別。record struct 之後的課會再細講 :P
class、struct、record 的比較
| 型別 | 預設不可變 | 值比較 | 容易解構 | 自動 ToString |
|---|---|---|---|---|
| class | 否 | 否(比參考) | 否 | 否 |
| struct | 否 | 是 | 否 | 否 |
| record | 是 | 是 | 是 | 是 |
9. 特點跟常見錯誤
很多新手會搞混 record 的細節。比如有時候以為改一個 record 物件的欄位,另一個也會跟著變(像 class 複製參考那樣)。不會啦!還有,寫 with 的時候要記得,永遠是產生新複本,不會改原本的。record 超適合用在你很在意資料乾淨、可預測的邏輯。
對了,如果你用 init 而不是 set 宣告欄位,也只能在建立時或用 with 設定,之後就不能改囉。
GO TO FULL VERSION