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

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

上述範例顯示參考型別的記錄與實值型別記錄之間的一些區別:

  • recordrecord class 宣告參考型別。 關鍵字 class 是選擇性的,但可以為讀取器增加清楚性。 宣告 record struct 實值型別。
  • 位置屬性在 和 中 record class是不可變readonly record struct 。 它們 可在record struct 變動。

本文的其餘部分將討論 record classrecord struct 類型。 每個區段都會詳細說明這些差異。 您應該在 和 之間 record class 決定 , record struct 類似于 在 和 struct 之間 class 決定。 字詞 記錄 是用來描述適用于所有記錄類型的行為。 record structrecord 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 classreadonly 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
}

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

實值相等

如果您未覆寫或取代相等方法,您宣告的類型會控管如何定義相等:

  • 針對 class 類型,如果兩個物件參考記憶體中的相同物件,則兩個物件相等。
  • 針對 struct 類型,如果兩個物件屬於相同類型,並儲存相同的值,則兩個物件相等。
  • 對於 record 類型,包括 record 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) 這個方法會做為靜態方法的基礎。

  • virtual、或 sealedEquals(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 變更 initset 存取子。

運算式的結果 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() 回的字串。 在下列範例中, ChildNamesSystem.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();
}

您可以自行實 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 ,即使 實例是 或 TeacherStudent 衍生型別也一樣。 實例具有相同的屬性和相同的屬性值。 但 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
}

泛型條件約束

不需要類型為記錄的泛型條件約束。 記錄滿足 classstruct 條件約束。 若要在特定記錄類型的階層上建立條件約束,請將條件約束放在基底記錄上,就像是基類一樣。 如需詳細資訊,請參閱 類型參數的條件約束

C# 語言規格

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

如需 C# 9 和更新版本中引進之功能的詳細資訊,請參閱下列功能提案附注:

另請參閱