C# 記錄類型

小提示

剛開始開發軟體嗎? 先從 入門 教學開始。 當你需要簡潔且內建等式的資料型態時,會遇到紀錄。

有其他語言的經驗嗎? C# 記錄類似於 Kotlin 中的資料類別或 Scala 中的案例類別。 它們是為儲存資料而優化的類型,並具備編譯器產生的等值, ToString以及複製語意。 record class瀏覽 vs record structwith expressions 區塊,找出 C# 專屬的模式。

record關鍵字是你對 a classstructa 施加的修飾符。 它告訴編譯器透過ToString表達式來產生值的相等性、格式化with和非破壞性變更。 底層型別(類別或結構體)仍決定實例是使用參考語意還是值語意。 修 record 飾符是在這些語意基礎上加入資料友善的行為。 當一個類型的主要角色是儲存資料,且兩個值相同的實例應視為相等時,使用記錄。

使用記錄的時機

當以下條件全部成立時,請使用紀錄:

  • 此類型的主要角色是儲存資料。
  • 兩個值相同的實例應該相等。
  • 你想要的是不可變性(尤其是針對record class型別)。
  • 你想要的是自動生成的可讀 ToString,而不是手動去寫一個。

record classrecord struct之間選擇時:

  • 當你需要繼承,或者當類型夠大,以至於每次作業都要複製而成本高昂時,請使用 record class
  • 使用 record struct 於小型且自包含的值,當中複製語義和堆疊分配具有優勢時。

避免在Entity Framework Core中記錄實體類型,因為這依賴參考相等來追蹤實體。 欲了解更廣泛的類型比較,請參閱「選擇哪種類型」。

宣告一筆紀錄

你可以將 record 套用到類別或結構。 最簡單的形式使用 位置參數 ,在一行中定義構造子與性質:

public record Person(string FirstName, string LastName);

相同的位置語法適用於 record struct 以下類型:

public record struct Coordinate(double Latitude, double Longitude);

public readonly record struct Temperature(double Celsius)
{
    public double Fahrenheit => Celsius * 9.0 / 5.0 + 32.0;
}

單獨寫 recordrecord class 的簡寫,而 record class 本身是一種參考類型。 寫作 record struct 創造一種價值類型。 編譯器在兩種情況下都是從位置參數產生屬性,但預設值有所不同:

  • record class:屬性僅限於 init(在建構後不可變)。
  • record struct:屬性預設為讀寫。 添加 readonlyreadonly record struct),使其成為僅限於 init

當你需要更多控制時,也可以用標準屬性語法來寫紀錄。 例如,要讓一個屬性從唯讀變為讀寫,可以這樣做:

public record Product
{
    public required string Name { get; init; }
    public decimal Price { get; set; }
}

record classrecord struct

由於 record 修飾符保留底層型別的語意,指派或比較參考時,a record class 和 a record struct 的行為不同。 分配一個 record class 是拷貝參考。 兩個變數都指向同一個物件。 指派 a record struct 會複製資料,因此對一個變數的變更不會影響另一個變數:

// Record class — assignment copies the reference
var p1 = new Person("Grace", "Hopper");
var p2 = p1; // p1 and p2 point to the same object:
Console.WriteLine(ReferenceEquals(p1, p2)); // True

// Record struct — assignment copies the data
var c1 = new Coordinate(47.6062, -122.3321);
var c2 = c1;
c2.Longitude = 0.0; // mutating c2 doesn't affect c1
Console.WriteLine(c1.Longitude); // -122.3321
Console.WriteLine(c2.Longitude); // 0

記錄結構也提供編譯器產生的值等性:

var home = new Coordinate(47.6062, -122.3321);
var copy = home;

Console.WriteLine(home);           // Coordinate { Latitude = 47.6062, Longitude = -122.3321 }
Console.WriteLine(home == copy);   // True — value equality

當需要繼承或實例大到複製成本高昂時,選擇 record class。 若資料是小型且自包含的,在適合使用數值型別複製語意的情況下,請選擇 record struct。 關於值型態語意的更多資訊,請參見 結構體

價值平等

修飾符為類別和結構提供由編譯器生成的逐屬性相等性。 以下是四種類型的平等運作方式:

  • 純類別:預設使用 參考等式 。 運算子 == 檢查的是兩個變數是否指向同一物件,而非資料是否相符。
  • 純結構:透過支援值相等性 ValueType.Equals,但預設實作使用反射,速度較慢且不會產生==/!= 運算子。
  • record class:編譯器會產生 Equals 方法和 GetHashCode 方法,以及 ==/!= 運算子,這些用以比較每個屬性值。 兩個擁有相同資料的不同物體是相等的。
  • record struct:與 record class 相同的編譯器生成等式,但不使用反射,這使得它比單純結構等式更快。

以下範例展示了記錄類別的相等性:

// Person is a record type with three properties: FirstName, LastName, and PhoneNumbers.
var phones = new string[] { "555-1234" };
var person1 = new Person("Grace", "Hopper", phones);
var person2 = new Person("Grace", "Hopper", phones);

Console.WriteLine(person1 == person2);              // True
Console.WriteLine(ReferenceEquals(person1, person2)); // False

person1.PhoneNumbers[0] = "555-9999";
Console.WriteLine(person2.PhoneNumbers[0]); // 555-9999 — same array

這兩個 Person 實例是不同的物件,但它們是相等的,因為它們的所有屬性值都相符。 陣列屬性是依參考比較,而非內容。 修改共享陣列在兩個紀錄中都可見,因為陣列本身沒有被複製。

非破壞性突變,表達 with 式為

紀錄通常是不可變的,所以建立後你無法更改屬性。 表達式 with 會產生一個複製品,但會改變一個或多個屬性,但原始記錄不變。 此方法適用於以下record classrecord struct兩種類型:

var original = new Person("Grace", "Hopper");
var modified = original with { FirstName = "Margaret" };

Console.WriteLine(original); // Person { FirstName = Grace, LastName = Hopper }
Console.WriteLine(modified); // Person { FirstName = Margaret, LastName = Hopper }
Console.WriteLine(original == modified); // False

var copy = original with { };
Console.WriteLine(original == copy); // True

相同的語法適用於 record struct 類型:

var shifted = home with { Longitude = -122.0 };
Console.WriteLine(shifted);        // Coordinate { Latitude = 47.6062, Longitude = -122 }
Console.WriteLine(home == shifted); // False

一個 with 表達式會複製現有實例,然後套用指定的屬性變更。

位置記錄與解構

位置記錄會產生 Deconstruct 一種方法,用來將屬性值提取成個別變數:

var (first, last) = person;
Console.WriteLine($"{first} {last}");
// Grace Hopper

解構同時可以與record classrecord struct類型一起運作。 你可以用它來做作業、 foreach 循環和模式匹配。

紀錄繼承

A record class 可以繼承另一個 record class。 紀錄無法繼承一般類別,類別也無法繼承記錄:

public record Student(string FirstName, string LastName, int GradeLevel)
    : Person(FirstName, LastName);

值等式檢查包含執行時類型,因此具有相同 PersonStudentFirstNameLastName 不會被視為相等。 記錄結構不支援繼承,因為結構體不能繼承其他類型。

另請參閱