記錄 (C# 參考)
您可以使用 record
修飾元定義參考型別,以提供內建功能來封裝資料。 C# 10 允許將 record class
語法作為同義字來闡明參考類型,並允許 record struct
定義具有類似功能的值類型。
當您在記錄上宣告主要建構函式時,編譯器會產生主要建構函式參數的公用屬性。 記錄的主要建構函式參數稱為「位置參數」。 編譯器會建立「位置屬性」,以鏡像主要建構函式或位置參數。 編譯器不會針對沒有 record
修飾元的型別來合成主要建構函式參數的屬性。
下列兩個範例示範 record
(或 record class
) 參考類型:
public record Person(string FirstName, string LastName);
public record Person
{
public required string FirstName { get; init; }
public required string LastName { get; init; }
};
下列兩個範例示範 record struct
值類型:
public readonly record struct Point(double X, double Y, double Z);
public record struct Point
{
public double X { get; init; }
public double Y { get; init; }
public double Z { get; init; }
}
您也可以建立具有可變屬性和欄位的記錄:
public record Person
{
public required string FirstName { get; set; }
public required string LastName { get; set; }
};
record struct 也可以是可變的 (位置記錄結構與沒有位置參數的記錄結構):
public record struct DataMeasurement(DateTime TakenAt, double Measurement);
public record struct Point
{
public double X { get; set; }
public double Y { get; set; }
public double Z { get; set; }
}
雖然記錄可以是可變的,但它們主要是用來支援不可變的資料模式。 記錄類型提供下列功能:
- 使用不可變屬性建立參考類型的精簡語法
- 適用於以資料為中心的參考類型的內建行為:
- 繼承階層的支援
上面的範例顯示了參考類型的記錄以及為值類型的記錄之間的一些區別:
record
或record class
宣告了參考類型。class
關鍵字是選用性的,但它可為讀者增加清楚性。record struct
宣告了值類型。- 位置屬性在
record class
及readonly record struct
中是不可變的。 它們在record struct
中是可變的。
本文的其餘部分會討論 record class
和 record struct
類型。 每個小節都會詳細說明這些差異。 您應該在 record class
與 record struct
之間做出決定,類似在 class
與 struct
之間做出決定一樣。 記錄 (record) 一詞是用來描述適用於所有記錄類型的行為。 record struct
或 record class
是分別用來描述僅適用於結構 (struct) 或類別 (class) 類型的行為。 record struct
型別已在 C# 10 中引進。
屬性定義的位置語法
您可以使用位置參數來宣告記錄的屬性,並在建立執行個體時初始化屬性值:
public record Person(string FirstName, string LastName);
public static void Main()
{
Person person = new("Nancy", "Davolio");
Console.WriteLine(person);
// output: Person { FirstName = Nancy, LastName = Davolio }
}
當您使用位置語法來進行屬性定義時,編譯器會建立:
- 記錄宣告中提供之每個位置參數的公用自動實作屬性。
- 對於
record
類型和readonly record struct
類型:init-only 屬性。 - 對於
record struct
類型:read-write 屬性。
- 對於
- 一個其參數符合記錄宣告上的位置參數的主要建構函式。
- 一個將每個欄位設為其預設值的無參數建構函式 (為 record struct 類型)。
- 一個含
out
參數的Deconstruct
方法 (為記錄宣告中所提供的每一個位置參數)。 方法會解構使用位置語法所定義的屬性;它會忽略使用標準屬性語法所定義的屬性。
您可能想要將屬性 (attribute) 新增到編譯器從記錄定義建立的這些任何元素。 您可以將目標 新增至您套用到位置記錄屬性 (property) 的任何屬性 (attribute)。 下列範例會將 System.Text.Json.Serialization.JsonPropertyNameAttribute 套用至 Person
記錄的每一個屬性 (property)。 property:
目標指出屬性 (attribute) 會套用至編譯器產生的屬性 (property)。 其他值是 field:
(將 attribute 套用至欄位),以及 param:
(將 attribute 套用至參數)。
/// <summary>
/// Person record type
/// </summary>
/// <param name="FirstName">First Name</param>
/// <param name="LastName">Last Name</param>
/// <remarks>
/// The person type is a positional record containing the
/// properties for the first and last name. Those properties
/// map to the JSON elements "firstName" and "lastName" when
/// serialized or deserialized.
/// </remarks>
public record Person([property: JsonPropertyName("firstName")] string FirstName,
[property: JsonPropertyName("lastName")] string LastName);
上面的範例也示範如何為記錄建立 XML 文件註解。 您可以加入 <param>
標記,以加入主要建構函式參數的文件註解。
如果產生的自動實作屬性定義不是您想要的,您可以自行定義同名的屬性。 例如,您可能想要變更可存取性或可變性,或提供 get
或 set
存取子的實作。 如果您在來源中宣告屬性,則必須從記錄的位置參數中來將其初始化。 如果您的屬性是自動實作屬性,則必須將該屬性初始化。 如果您在來源中新增一個支援欄位,則必須將該支援欄位初始化。 產生的解構函式會使用您的屬性定義。 例如,下列範例會將位置記錄的 FirstName
和 LastName
屬性宣告為 public
,但會將 Id
位置參數限制為 internal
。 您可以針對記錄和記錄結構類型使用此語法。
public record Person(string FirstName, string LastName, string Id)
{
internal string Id { get; init; } = Id;
}
public static void Main()
{
Person person = new("Nancy", "Davolio", "12345");
Console.WriteLine(person.FirstName); //output: Nancy
}
記錄類型不需要宣告任何的位置屬性。 您可以宣告不含任何位置屬性的記錄,也可以宣告其他欄位和屬性,如下列範例所示:
public record Person(string FirstName, string LastName)
{
public string[] PhoneNumbers { get; init; } = [];
};
如果您使用標準屬性語法來定義屬性,但省略存取修飾詞,則屬性會隱含地設為 private
。
不變性
位置記錄和位置唯讀記錄結構會宣告 init-only 屬性。 位置記錄結構會宣告 read-write 屬性。 您可以覆寫這些預設值中的任何一個,如上節所示。
當您需要以資料為中心的類型為安全執行緒,或您相依於雜湊表中保持不變的雜湊碼時,則不變性可能非常有用。 不過,不變性並不適用於所有的資料情況。 例如,Entity Framework Core 不支援使用不可變的實體類型來進行更新。
Init-only 屬性 (無論是從位置參數 (record class
和 readonly record struct
) 建立,還是藉由指定 init
存取子建立) 都具有淺層不變性。 在初始化之後,您無法變更 value-type 屬性的值或 reference-type 屬性的參考。 不過,reference-type 屬性所參考的資料可以變更。 下列範例顯示 reference-type 不可變屬性的內容 (在本例中為陣列) 是可變的:
public record Person(string FirstName, string LastName, string[] PhoneNumbers);
public static void Main()
{
Person person = new("Nancy", "Davolio", new string[1] { "555-1234" });
Console.WriteLine(person.PhoneNumbers[0]); // output: 555-1234
person.PhoneNumbers[0] = "555-6789";
Console.WriteLine(person.PhoneNumbers[0]); // output: 555-6789
}
記錄類型特有的功能是由編譯器合成方法所實作,而且這些方法都不會藉由修改物件狀態來危害不變性。 除非指定,否則會針對 record
、record struct
和 readonly record struct
宣告來產生合成方法。
實值相等
如果您未覆寫或取代相等方法,則您所宣告的類型決定了相等的定義方式:
- 對於
class
類型,如果兩個物件參考記憶體中的相同物件,則這兩個物件相等。 - 對於
struct
類型,如果兩個物件屬於相同類型並儲存相同的值,則這兩個物件相等。 - 對於具有
record
修飾元的型別 (record class
、record struct
和readonly record struct
),如果兩個物件屬於相同型別並儲存相同的值,則這兩個物件相等。
record struct
的相等定義與 struct
的相等定義相同。 不同之處在於,對於 struct
,實作是在 ValueType.Equals(Object) 中並依賴於反映。 對於記錄,實作是編譯器合成,並使用宣告的資料成員。
某些資料模式需要參考相等。 例如,Entity Framework Core 相依於參考相等,以確保它只使用實體類型的一個執行個體來表示概念上的一個實體。 因此,記錄和記錄結構不適合在 Entity Framework Core 中用作實體類型。
下列範例說明記錄類型的值相等:
public record Person(string FirstName, string LastName, string[] PhoneNumbers);
public static void Main()
{
var phoneNumbers = new string[2];
Person person1 = new("Nancy", "Davolio", phoneNumbers);
Person person2 = new("Nancy", "Davolio", phoneNumbers);
Console.WriteLine(person1 == person2); // output: True
person1.PhoneNumbers[0] = "555-1234";
Console.WriteLine(person1 == person2); // output: True
Console.WriteLine(ReferenceEquals(person1, person2)); // output: False
}
為了實作值相等,編譯器會合成幾種方法,包括:
Object.Equals(Object) 的覆寫。 如果明確宣告覆寫,則為錯誤。
當兩個參數都是非 Null 時,此方法會用作 Object.Equals(Object, Object) 靜態方法的基礎。
R
是記錄類型的virtual
(或sealed
)Equals(R? other)
。 這個方法會實作 IEquatable<T>。 這個方法可以明確宣告。如果記錄類型衍生自基底記錄類型
Base
,則Equals(Base? other)
。 如果明確宣告覆寫,則為錯誤。 如果您提供自己的Equals(R? other)
實作,請同時提供GetHashCode
的實作。Object.GetHashCode() 的覆寫。 這個方法可以明確宣告。
運算子
==
和!=
的覆寫。 如果明確宣告運算子,則為錯誤。如果記錄類型衍生自基底記錄類型,則
protected override Type EqualityContract { get; };
。 這個屬性可以明確宣告。 如需詳細資訊,請參閱繼承階層中的相等。
如果記錄型別具有一個與允許明確宣告之合成方法簽章相符的方法,則編譯器不會合成該方法。
非破壞性變異
如果您需要複製具有某些修改的執行個體,您可以使用 with
運算式來達成非破壞性的變異。 with
運算式會建立一個新的記錄執行個體,該執行個體是現有記錄執行個體的複本,並修改了指定的屬性和欄位。 您可以使用物件初始設定式語法來指定要變更的值,如下列範例所示:
public record Person(string FirstName, string LastName)
{
public string[] PhoneNumbers { get; init; }
}
public static void Main()
{
Person person1 = new("Nancy", "Davolio") { PhoneNumbers = new string[1] };
Console.WriteLine(person1);
// output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] }
Person person2 = person1 with { FirstName = "John" };
Console.WriteLine(person2);
// output: Person { FirstName = John, LastName = Davolio, PhoneNumbers = System.String[] }
Console.WriteLine(person1 == person2); // output: False
person2 = person1 with { PhoneNumbers = new string[1] };
Console.WriteLine(person2);
// output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] }
Console.WriteLine(person1 == person2); // output: False
person2 = person1 with { };
Console.WriteLine(person1 == person2); // output: True
}
with
運算式可以使用標準屬性語法來設定位置屬性或所建立的屬性。 明確宣告的屬性必須具有要在 with
運算式中變更的 init
或 set
存取子。
with
運算式的結果是一個淺層複本,這表示針對參考屬性,只會複製執行個體的參考。 原始記錄和複本最後都會具有同一個執行個體的參考。
為了實作 record class
類型的這項功能,編譯器會合成一個 clone 方法和一個 copy 建構函式。 虛擬複製方法會傳回 copy 建構函式初始化的新記錄。 當您使用 with
運算式時,編譯器會建立呼叫 clone 方法的程式碼,然後設定 with
運算式中所指定的屬性。
如果您需要不同的複製行為,您可以在 record class
中撰寫自己的 copy 建構函式。 如果您這樣做,編譯器不會進行合成。 如果記錄是 sealed
,則會將您的建構函式設為 private
,否則將其設為 protected
。 編譯器不會為 record struct
類型合成 copy 建構函式。 您可以撰寫一個,但編譯器不會為 with
運算式產生對它的呼叫。 record struct
的值會在指派時複製。
您無法覆寫 clone 方法,也不能在任何記錄類型中建立一個名為 Clone
的成員。 clone 方法的實際名稱是編譯器產生的。
用於顯示的內建格式設定
記錄類型有一個編譯器產生的 ToString 方法,可顯示公開屬性和欄位的名稱和值。 ToString
方法會傳回下列格式的字串:
<record type name> { <property name> = <value>, <property name> = <value>, ...}
為 <value>
列印的字串是 ToString() 為屬性類型所傳回的字串。 在下列範例中,ChildNames
是 System.Array,其中 ToString
會傳回 System.String[]
:
Person { FirstName = Nancy, LastName = Davolio, ChildNames = System.String[] }
為了實作此功能,在 record class
類型中,編譯器會合成了一個虛擬 PrintMembers
方法和一個 ToString 覆寫。 在 record struct
類型中,此成員為 private
。
ToString
覆寫會建立一個 StringBuilder 物件,其類型名稱後面接著一個左括弧。 它會呼叫 PrintMembers
以新增屬性名稱和值,然後加入右括弧。 以下範例顯示的程式碼類似於合成覆寫所包含的內容:
public override string ToString()
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("Teacher"); // type name
stringBuilder.Append(" { ");
if (PrintMembers(stringBuilder))
{
stringBuilder.Append(" ");
}
stringBuilder.Append("}");
return stringBuilder.ToString();
}
您可以提供自己的 PrintMembers
或 ToString
覆寫的實作。 本文後面的衍生記錄中的 PrintMembers
格式設定一節提供了範例。 在 C# 10 和更新版本中,您的 ToString
實作可能包含 sealed
修飾詞,這會阻止編譯器為任何衍生的記錄合成 ToString
實作。 您可以在整個 record
型別階層中建立一致的字串表示。 (衍生的記錄仍具有針對所有衍生屬性產生的 PrintMembers
方法。)
繼承
本節僅適用於使用 record class
類型。
記錄可以繼承自另一個記錄。 不過,記錄無法繼承自類別,而類別無法繼承自記錄。
衍生記錄類型中的位置參數
衍生的記錄會為基底記錄主要建構函式中所有參數宣告位置參數。 基底記錄會宣告並初始化這些屬性。 衍生的記錄不會隱藏它們,但只會為其基底記錄中未宣告的參數建立並初始化屬性。
下列範例說明使用位置屬性語法的繼承:
public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", 3);
Console.WriteLine(teacher);
// output: Teacher { FirstName = Nancy, LastName = Davolio, Grade = 3 }
}
繼承階層中的相等
本節適用於 record class
類型,但不適用於 record struct
類型。 若要讓兩個記錄變數相等,執行階段類型必須相等。 包含變數的類型可能不同。 下列程式碼範例說明了繼承的相等比較:
public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", 3);
Person student = new Student("Nancy", "Davolio", 3);
Console.WriteLine(teacher == student); // output: False
Student student2 = new Student("Nancy", "Davolio", 3);
Console.WriteLine(student2 == student); // output: True
}
在此範例中,所有變數都會宣告為 Person
,即使在執行個體是 Student
或 Teacher
的衍生類型時也一樣。 執行個體具有相同的屬性和相同的屬性值。 但是 student == teacher
會傳回 False
(雖然兩個都是 Person
類型的變數),而 student == student2
會傳回 True
(雖然一個是 Person
變數,一個是 Student
變數)。 相等測試取決於實際物件的執行階段類型,而不是變數的宣告類型。
為了實作值相等,編譯器會合成一個 EqualityContract
屬性,該屬性會傳回一個符合記錄類型的 Type 物件。 EqualityContract
會啟用相等方法,以在它們檢查是否相等時比較物件的執行階段類型。 如果記錄的基底類型為 object
,則此屬性為 virtual
。 如果基底類型是另一個記錄類型,則此屬性為覆寫。 如果記錄類型為 sealed
,則此屬性實際上是 sealed
,因為類型為 sealed
。
當程式碼比較衍生型別的兩個執行個體時,合成的相等方法會檢查基底和衍生型別的所有資料成員是否相等。 合成的 GetHashCode
方法會使用基底型別和衍生記錄型別中宣告之所有資料成員中的 GetHashCode
方法。 record
的資料成員包含所有宣告的欄位,以及任何自動實作屬性的編譯器合成支援欄位。
衍生記錄中的 with
運算式
with
運算式的結果與運算式的運算元具有相同的執行階段類型。 會複製執行階段類型的所有屬性,但您只能設定編譯時間類型的屬性,如下列範例所示:
public record Point(int X, int Y)
{
public int Zbase { get; set; }
};
public record NamedPoint(string Name, int X, int Y) : Point(X, Y)
{
public int Zderived { get; set; }
};
public static void Main()
{
Point p1 = new NamedPoint("A", 1, 2) { Zbase = 3, Zderived = 4 };
Point p2 = p1 with { X = 5, Y = 6, Zbase = 7 }; // Can't set Name or Zderived
Console.WriteLine(p2 is NamedPoint); // output: True
Console.WriteLine(p2);
// output: NamedPoint { X = 5, Y = 6, Zbase = 7, Name = A, Zderived = 4 }
Point p3 = (NamedPoint)p1 with { Name = "B", X = 5, Y = 6, Zbase = 7, Zderived = 8 };
Console.WriteLine(p3);
// output: NamedPoint { X = 5, Y = 6, Zbase = 7, Name = B, Zderived = 8 }
}
衍生記錄中的 PrintMembers
格式設定
衍生記錄類型的合成 PrintMembers
方法會呼叫基底實作。 結果是衍生和基底類型的所有公開屬性和欄位都包含在 ToString
輸出中,如下列範例所示:
public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", 3);
Console.WriteLine(teacher);
// output: Teacher { FirstName = Nancy, LastName = Davolio, Grade = 3 }
}
您可以提供自己的 PrintMembers
方法的實作。 如果您這樣做,請使用下列簽章:
- 對於衍生自
object
的sealed
記錄 (未宣告基底記錄):private bool PrintMembers(StringBuilder builder)
; - 對於衍生自另一個記錄的
sealed
記錄 (請注意,封入類型是sealed
,因方法實際上為sealed
):protected override bool PrintMembers(StringBuilder builder)
; - 對於不是
sealed
且衍生自物件的記錄:protected virtual bool PrintMembers(StringBuilder builder);
- 對於不是
sealed
且衍生自另一個記錄的記錄:protected override bool PrintMembers(StringBuilder builder);
以下是取代合成 PrintMembers
方法的程式碼範例 (一個用於衍生自物件的記錄類型,而另一個用於衍生自另一個記錄的記錄類型):
public abstract record Person(string FirstName, string LastName, string[] PhoneNumbers)
{
protected virtual bool PrintMembers(StringBuilder stringBuilder)
{
stringBuilder.Append($"FirstName = {FirstName}, LastName = {LastName}, ");
stringBuilder.Append($"PhoneNumber1 = {PhoneNumbers[0]}, PhoneNumber2 = {PhoneNumbers[1]}");
return true;
}
}
public record Teacher(string FirstName, string LastName, string[] PhoneNumbers, int Grade)
: Person(FirstName, LastName, PhoneNumbers)
{
protected override bool PrintMembers(StringBuilder stringBuilder)
{
if (base.PrintMembers(stringBuilder))
{
stringBuilder.Append(", ");
};
stringBuilder.Append($"Grade = {Grade}");
return true;
}
};
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", new string[2] { "555-1234", "555-6789" }, 3);
Console.WriteLine(teacher);
// output: Teacher { FirstName = Nancy, LastName = Davolio, PhoneNumber1 = 555-1234, PhoneNumber2 = 555-6789, Grade = 3 }
}
注意
在 C# 10 和更新版本中,即使基底記錄已密封 PrintMembers
方法,編譯器也會在衍生記錄中合成 ToString
。 您也可以建立自己的 PrintMembers
實作。
衍生記錄中的解構函式行為
衍生記錄的 Deconstruct
方法會傳回編譯時間類型的所有位置屬性值。 如果變數類型是基底記錄,除非物件轉換成衍生類型,否則只會解構基底記錄屬性。 下列範例示範如何在衍生記錄上呼叫解構函式。
public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", 3);
var (firstName, lastName) = teacher; // Doesn't deconstruct Grade
Console.WriteLine($"{firstName}, {lastName}");// output: Nancy, Davolio
var (fName, lName, grade) = (Teacher)teacher;
Console.WriteLine($"{fName}, {lName}, {grade}");// output: Nancy, Davolio, 3
}
泛型條件約束
record
關鍵字是 class
或 struct
型別的修飾元。 新增 record
修飾元包含此文章先前描述的行為。 沒有需要類型為記錄的泛型條件約束。 record class
滿足 class
限制式。 record struct
滿足 struct
限制式。 如需詳細資訊,請參閱類型參數上的條件約束。
C# 語言規格
如需這些功能的詳細資訊,請參閱下列功能提議說明: