記錄 (C# 參考)
從 C# 9 開始,您可以使用 record
關鍵字來定義 參考型 別,以提供內建功能來封裝資料。 C# 10 允許 record class
語法做為同義字來厘清參考型別,並 record struct
定義具有類似功能的 實值型 別。 您可以使用位置參數或標準屬性語法,建立具有不可變屬性的記錄類型。
下列兩個範例示範 record
(或 record class
) 參考類型:
public record Person(string FirstName, string LastName);
public record Person
{
public string FirstName { get; init; } = default!;
public string LastName { get; init; } = default!;
};
下列兩個範例示範 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 string FirstName { get; set; } = default!;
public string LastName { get; set; } = default!;
};
記錄結構也可以變動,位置記錄結構與沒有位置參數的記錄結構:
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
類似于 在 和 struct
之間 class
決定。 字詞 記錄 是用來描述適用于所有記錄類型的行為。 record struct
或 record class
分別用來描述只套用至結構或類別類型的行為。 類型 record
是在 C# 9 中引進; 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 屬性。 - 針對
record struct
類型:讀寫屬性。
- 針對
- 主要建構函式,其參數符合記錄宣告上的位置參數。
- 針對記錄結構類型,無參數建構函式會將每個欄位設定為其預設值。
- 方法
Deconstruct
,其中包含out
記錄宣告中提供之每個位置參數的參數。 方法會解構使用位置語法所定義的屬性;它會忽略使用標準屬性語法定義的屬性。
您可能會想要將屬性新增至編譯器從記錄定義建立的任何這些專案。 您可以將 目標 新增至套用至位置記錄屬性的任何屬性。 下列範例會將 套 System.Text.Json.Serialization.JsonPropertyNameAttribute 用至記錄的每個屬性 Person
。 目標 property:
表示屬性會套用至編譯器產生的屬性。 其他值是 field:
將 屬性套用至 欄位,並將 param:
屬性套用至 參數。
/// <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>
標記,以新增主要建構函式參數的檔。
如果產生的自動實作屬性定義不是您想要的屬性,您可以定義同名自己的屬性。 例如,您可能想要變更協助工具或可變性,或提供 或 set
存取子的 get
實作。 如果您在來源中宣告 屬性,則必須從記錄的位置參數初始化它。 如果您的屬性是自動實作的屬性,您必須初始化 屬性。 如果您在來源中新增備份欄位,則必須初始化備份欄位。 產生的解構函式會使用您的屬性定義。 例如,下列範例會宣告 FirstName
位置記錄 public
的 和 LastName
屬性,但會將 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; } = Array.Empty<string>();
};
如果您使用標準屬性語法來定義屬性,但省略存取修飾詞,則屬性會隱含地 private
為 。
不變性
位置記錄和位置唯讀記錄結構宣告僅限 init 屬性。 位置記錄結構會宣告讀寫屬性。 您可以覆寫上述其中一個預設值,如上一節所示。
當您需要以資料為中心的類型為安全線程,或視雜湊表中剩餘的雜湊程式碼而定,不變性可能會很有用。 不過,不適用於所有資料案例的不變性。 例如,Entity Framework Core不支援使用不可變的實體類型進行更新。
僅限 Init 屬性,無論是從位置參數建立 (record class
和 readonly record struct
) ,還是透過指定 init
存取子,都具有 淺層不變性。 初始化之後,您無法變更實數值型別屬性的值或參考型別屬性的參考。 不過,可以變更參考型別屬性所參考的資料。 下列範例顯示參考類型不可變屬性的內容 (陣列,在此案例中) 是可變的:
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 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) 這個方法會做為靜態方法的基礎。
virtual
、或sealed
,Equals(R? other)
其中R
是記錄類型。 這個方法會實作 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
類型的這項功能,編譯器會合成複製方法和複製建構函式。 虛擬複製方法會傳回復制建構函式初始化的新記錄。 當您使用 with
運算式時,編譯器會建立程式碼來呼叫 clone 方法,然後設定運算式中指定的 with
屬性。
如果您需要不同的複製行為,您可以在 中 record class
撰寫自己的複製建構函式。 如果您這樣做,編譯器將不會合成一個。 如果記錄為 sealed
,請建立建構函 private
式,否則將其設為 protected
。 編譯器不會合成類型的複製建構函 record struct
式。 您可以撰寫一個,但編譯器不會針對 with
運算式產生對它的呼叫。 的值 record struct
會在指派時複製。
您無法覆寫複製方法,而且無法在任何記錄類型中建立名為 Clone
的成員。 複製方法的實際名稱是編譯器產生的。
用於顯示的內建格式設定
記錄類型具有編譯器產生的 ToString 方法,可顯示公用屬性和欄位的名稱和值。 方法 ToString
會傳回下列格式的字串:
<記錄類型名稱 { < 屬性名稱 >> = < 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
,即使 實例是 或 Teacher
的 Student
衍生型別也一樣。 實例具有相同的屬性和相同的屬性值。 但 student == teacher
傳回 False
雖然兩者都是 Person
-type 變數,但 student == student2
傳回 True
,雖然其中一個 Person
是變數,另一個 Student
則是變數。 相等測試取決於實際物件的執行時間類型,而不是變數的宣告類型。
為了實作此行為,編譯器會 EqualityContract
合成屬性,該屬性會傳回 Type 符合記錄類型的 物件。 EqualityContract
可讓相等方法在檢查是否相等時比較物件的執行時間類型。 如果記錄的基底類型為 object
,則此屬性為 virtual
。 如果基底類型是另一個記錄類型,則此屬性為覆寫。 如果記錄類型為 sealed
,則此屬性實際上是 sealed
因為類型為 sealed
。
比較衍生型別的兩個實例時,合成相等方法會檢查基底和衍生型別的所有屬性是否相等。 合成 GetHashCode
方法會 GetHashCode
使用基底類型和衍生記錄類型中宣告之所有屬性和欄位的 方法。
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
。 如果您這樣做,請使用下列簽章:
sealed
對於衍生自object
(的記錄,不會宣告基底記錄) :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 和更新版本中,即使基底記錄已密封 ToString
方法,編譯器也會在衍生記錄中合成 PrintMembers
。 您也可以建立自己的 實作 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
}
泛型條件約束
不需要類型為記錄的泛型條件約束。 記錄滿足 class
或 struct
條件約束。 若要在特定記錄類型的階層上建立條件約束,請將條件約束放在基底記錄上,就像是基類一樣。 如需詳細資訊,請參閱 類型參數的條件約束。
C# 語言規格
如需 C# 9 和更新版本中引進之功能的詳細資訊,請參閱下列功能提案附注: