記錄 (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; }
}

雖然記錄可以是可變的,但它們主要是用來支援不可變的資料模式。 記錄類型提供下列功能:

上面的範例顯示了參考類型的記錄以及為值類型的記錄之間的一些區別:

  • recordrecord class 宣告了參考類型。 class 關鍵字是選用性的,但它可為讀者增加清楚性。 record struct 宣告了值類型。
  • 位置屬性在 record classreadonly record struct 中是不可變的。 它們在 record struct 中是可變的

本文的其餘部分會討論 record classrecord struct 類型。 每個小節都會詳細說明這些差異。 您應該在 record classrecord struct 之間做出決定,類似在 classstruct 之間做出決定一樣。 記錄 (record) 一詞是用來描述適用於所有記錄類型的行為。 record structrecord 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> 標記,以加入主要建構函式參數的文件註解。

如果產生的自動實作屬性定義不是您想要的,您可以自行定義同名的屬性。 例如,您可能想要變更可存取性或可變性,或提供 getset 存取子的實作。 如果您在來源中宣告屬性,則必須從記錄的位置參數中來將其初始化。 如果您的屬性是自動實作屬性,則必須將該屬性初始化。 如果您在來源中新增一個支援欄位,則必須將該支援欄位初始化。 產生的解構函式會使用您的屬性定義。 例如,下列範例會將位置記錄的 FirstNameLastName 屬性宣告為 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 classreadonly 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
}

記錄類型特有的功能是由編譯器合成方法所實作,而且這些方法都不會藉由修改物件狀態來危害不變性。 除非指定,否則會針對 recordrecord structreadonly record struct 宣告來產生合成方法。

實值相等

如果您未覆寫或取代相等方法,則您所宣告的類型決定了相等的定義方式:

  • 對於 class 類型,如果兩個物件參考記憶體中的相同物件,則這兩個物件相等。
  • 對於 struct 類型,如果兩個物件屬於相同類型並儲存相同的值,則這兩個物件相等。
  • 對於具有 record 修飾元的型別 (record classrecord structreadonly 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 運算式中變更的 initset 存取子。

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() 為屬性類型所傳回的字串。 在下列範例中,ChildNamesSystem.Array,其中 ToString 會傳回 System.String[]

Person { FirstName = Nancy, LastName = Davolio, ChildNames = System.String[] }

為了實作此功能,在 record class 類型中,編譯器會合成了一個虛擬 PrintMembers 方法和一個 ToString 覆寫。 在 record struct 類型中,此成員為 privateToString 覆寫會建立一個 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();
}

您可以提供自己的 PrintMembersToString 覆寫的實作。 本文後面的衍生記錄中的 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,即使在執行個體是 StudentTeacher 的衍生類型時也一樣。 執行個體具有相同的屬性和相同的屬性值。 但是 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 方法的實作。 如果您這樣做,請使用下列簽章:

  • 對於衍生自 objectsealed 記錄 (未宣告基底記錄):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 關鍵字是 classstruct 型別的修飾元。 新增 record 修飾元包含此文章先前描述的行為。 沒有需要類型為記錄的泛型條件約束。 record class 滿足 class 限制式。 record struct 滿足 struct 限制式。 如需詳細資訊,請參閱類型參數上的條件約束

C# 語言規格

如需詳細資訊,請參閱 C# 語言規格類別一節。

如需這些功能的詳細資訊,請參閱下列功能提議說明:

另請參閱