CodeGym /課程 /C# SELF /認識 record 跟位置語法

認識 record 跟位置語法

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

1. 資料傳遞的問題

假設我們有個學校的應用程式,要在不同模組之間傳遞學生資訊:名字、出生年、班級。通常我們會怎麼做?

我們會寫個這樣的 class:


public class Student
{
    public string Name { get; set; }
    public int YearOfBirth { get; set; }
    public string Class { get; set; }
}
學生資料 class(舊方法)

看起來很正常,但這種寫法有幾個問題:

  • 如果要比較兩個學生是不是一樣,預設只會比物件參考。也就是說兩個欄位一樣但是不同物件的學生,還是不一樣!
  • class 建立後還可以被改,這有時候會出錯(尤其物件已經被別的地方用到);
  • 一堆「樣板」程式碼:建構子、比較方法、複製(clone)方法、ToString

你大概已經猜到了:C# 可以幫我們解決這些麻煩!來認識一下 record 吧。

2. 什麼是 record

record 是 C# 裡專門為資料儲存設計的一種型別。record 有兩個超重要的特點:

  1. 不可變性(immutability): record 物件預設是不可變的,也就是說屬性值只能在建立時設定一次,之後就不能改(其實 set 是 private)。雖然你也可以宣告可變的 record,但預設就是不可變。
  2. 值比較: 如果兩個 record 物件所有欄位值都一樣,它們就被視為相等(==.Equals() 會不一樣喔!)。

其實 record 超適合拿來在應用程式不同層之間傳資料(像是從資料庫到 controller,從 controller 到 view 之類的)。

3. record 的語法

最簡單的方式 — 位置語法

如果只是要傳一組值,可以用一行就宣告型別:


public record Student(string Name, int YearOfBirth, string Class);
位置語法的 record

底層發生什麼事?編譯器會自動幫你產生:

  • 只有 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 物件的值比較

也就是說,兩個欄位一樣的 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);
record — 一行就搞定所有事!

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 的解構

編譯器會自動幫每個位置 record 產生 Deconstruct 方法,這對 LINQ、switch pattern 超方便,整個人生都美好了。

8. record 是 class 還是 struct?

預設 record參考型別,跟 class 一樣。也就是說所有參考型別的特性(堆積儲存、複製參考等等)都一樣。

如果你想要值型別(value type),C# 也有對應的語法,可以這樣寫:


public record struct Point(int X, int Y);
record struct — 值型別的 record

但大部分傳資料還是用經典的 record,也就是參考型別。record struct 之後的課會再細講 :P

classstructrecord 的比較

型別 預設不可變 值比較 容易解構 自動 ToString
class 否(比參考)
struct
record

9. 特點跟常見錯誤

很多新手會搞混 record 的細節。比如有時候以為改一個 record 物件的欄位,另一個也會跟著變(像 class 複製參考那樣)。不會啦!還有,寫 with 的時候要記得,永遠是產生新複本,不會改原本的。record 超適合用在你很在意資料乾淨、可預測的邏輯。

對了,如果你用 init 而不是 set 宣告欄位,也只能在建立時或用 with 設定,之後就不能改囉。

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