小提示
剛開始開發軟體嗎? 先從 入門 教學開始。 當你需要簡潔且內建等式的資料型態時,會遇到紀錄。
有其他語言的經驗嗎? C# 記錄類似於 Kotlin 中的資料類別或 Scala 中的案例類別。 它們是為儲存資料而優化的類型,並具備編譯器產生的等值, ToString以及複製語意。
record class瀏覽 vs record struct 和 with expressions 區塊,找出 C# 專屬的模式。
record關鍵字是你對 a class 或 structa 施加的修飾符。 它告訴編譯器透過ToString表達式來產生值的相等性、格式化with和非破壞性變更。 底層型別(類別或結構體)仍決定實例是使用參考語意還是值語意。 修 record 飾符是在這些語意基礎上加入資料友善的行為。 當一個類型的主要角色是儲存資料,且兩個值相同的實例應視為相等時,使用記錄。
使用記錄的時機
當以下條件全部成立時,請使用紀錄:
- 此類型的主要角色是儲存資料。
- 兩個值相同的實例應該相等。
- 你想要的是不可變性(尤其是針對
record class型別)。 - 你想要的是自動生成的可讀
ToString,而不是手動去寫一個。
在record class和record 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;
}
單獨寫 record 是 record class 的簡寫,而 record class 本身是一種參考類型。 寫作 record struct 創造一種價值類型。 編譯器在兩種情況下都是從位置參數產生屬性,但預設值有所不同:
-
record class:屬性僅限於init(在建構後不可變)。 -
record struct:屬性預設為讀寫。 添加readonly(readonly record struct),使其成為僅限於init。
當你需要更多控制時,也可以用標準屬性語法來寫紀錄。 例如,要讓一個屬性從唯讀變為讀寫,可以這樣做:
public record Product
{
public required string Name { get; init; }
public decimal Price { get; set; }
}
record class 與 record 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 class和record struct類型一起運作。 你可以用它來做作業、 foreach 循環和模式匹配。
紀錄繼承
A record class 可以繼承另一個 record class。 紀錄無法繼承一般類別,類別也無法繼承記錄:
public record Student(string FirstName, string LastName, int GradeLevel)
: Person(FirstName, LastName);
值等式檢查包含執行時類型,因此具有相同 Person 和 Student 的 FirstName 和 LastName 不會被視為相等。 記錄結構不支援繼承,因為結構體不能繼承其他類型。