記錄 (c # 參考)

從 c # 9 開始,您可以使用 record 關鍵字來定義 參考型 別,以提供內建功能來封裝資料。 您可以使用位置參數或標準屬性語法,建立具有不可變屬性的記錄類型:

public record Person(string FirstName, string LastName);
public record Person
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
};

您也可以使用可變屬性和欄位來建立記錄類型:

public record Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
};

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

您也可以使用 結構類型 來設計以資料為中心的型別,以提供實值相等且幾乎不會有任何行為。 但對於相對較大的資料模型,結構類型有一些缺點:

  • 它們不支援繼承。
  • 它們在判斷值是否相等時的效率較低。 如果是實值型別, ValueType.Equals 方法會使用反映來尋找所有欄位。 若為記錄,編譯器會產生 Equals 方法。 在實務上,記錄中的實值相等顯著會更快。
  • 在某些情況下,它們會使用更多的記憶體,因為每個實例都有所有資料的完整複本。 記錄類型是 參考型別,因此記錄實例只包含資料的參考。

屬性定義的位置語法

您可以使用位置參數來宣告記錄的屬性,以及在建立實例時初始化屬性值:

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 }
}

當您使用屬性定義的位置語法時,編譯器會建立:

  • 記錄宣告中所提供之每個位置參數的公用僅限初始自動執行屬性。 僅限初始化的屬性只能在函 中設定,或是使用屬性初始化運算式來設定。
  • 主要的函式,其參數符合記錄宣告上的位置參數。
  • Deconstruct具有 out 在記錄宣告中提供的每個位置參數之參數的方法。 只有在有兩個或多個位置參數時,才會提供這個方法。 方法會解構為使用位置語法定義的屬性;它會忽略使用標準屬性語法所定義的屬性。

您可能會想要將屬性新增至編譯器從記錄定義建立的任何元素。 您可以將 目標 新增至套用至位置記錄屬性的任何屬性。 下列範例會將套用 System.Text.Json.Serialization.JsonPropertyNameAttribute 至記錄的每個屬性 Personproperty:目標表示屬性已套用至編譯器產生的屬性。 其他值則是將 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> 標記,以新增主要函式參數的檔。

如果產生的自動執行屬性定義不是您想要的,您可以定義您自己的相同名稱屬性。 如果您這樣做,所產生的函式和解構函式會使用您的屬性定義。 例如,下列範例會建立 FirstName 位置屬性 internal 而不是 public

public record Person(string FirstName, string LastName)
{
    internal string FirstName { get; init; } = FirstName;
}

public static void Main()
{
    Person person = new("Nancy", "Davolio");
    Console.WriteLine(person.FirstName); //output: Nancy
}

記錄類型不需要宣告任何位置屬性。 您可以宣告沒有任何位置屬性的記錄,也可以宣告其他欄位和屬性,如下列範例所示:

public record Person(string FirstName, string LastName)
{
    public string[] PhoneNumbers { get; init; }
};

如果您使用標準屬性語法來定義屬性,但省略存取修飾詞,則會隱含地屬性 public

不變性

記錄類型不一定是不可變的。 您可以使用非 set 的存取子和欄位來宣告屬性 readonly 。 但是雖然記錄可以是可變動的,但可讓您更輕鬆地建立不可變的資料模型。

當您需要以資料為中心的型別為安全線程,或者您是根據雜湊表中剩餘的雜湊碼而定時,永久性可能很有用。 但是,不適合所有資料案例的永久性。 例如, Entity Framework Core不支援使用不可變的實體類型進行更新。

僅限初始屬性(不論是從位置參數建立)還是藉由指定 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
}

記錄類型獨有的功能是由編譯器合成的方法所執行,而且這些方法都不會藉由修改物件狀態來危及非可變性。

實值相等

值相等表示如果類型相符且所有屬性和域值相符,則記錄類型的兩個變數會相等。 對於其他參考型別,相等表示身分識別。 也就是說,如果參考型別的兩個變數參考相同的物件,就會相等。

某些資料模型需要參考相等。 例如, 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
}

為了實值相等,編譯器會會合成下列方法:

class 類型中,您可以手動覆寫等號比較方法和運算子來達成值的相等,但開發和測試該程式碼會相當耗時且容易出錯。 有了這項功能內建功能,可防止在新增或變更屬性或欄位時,忘記更新自訂覆寫程式碼所造成的錯誤。

您可以撰寫自己的實作為來取代任何這些合成方法。 如果記錄類型的方法符合任何合成方法的簽章,則編譯器不會合成該方法。

如果您在記錄類型中提供自己的實作為 Equals ,也請提供的實作為 GetHashCode

非破壞性變化

如果您需要改變記錄實例的不可變屬性,您可以使用 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運算式可以設定位置屬性,或使用標準屬性語法建立的屬性。 非位置屬性必須 init set 在運算式中變更或存取子 with

運算式的結果 with淺層複製,這表示對於參考屬性,只會複製實例的參考。 原始記錄和複製最後都有相同實例的參考。

為了執行這項功能,編譯器會會合成複製方法和複製的函式。 此函式會使用要複製的記錄實例,並呼叫 clone 方法。 當您使用 with 運算式時,編譯器會建立呼叫複製函式的程式碼,然後設定運算式中所指定的屬性 with

如果您需要不同的複製行為,您可以撰寫自己的複製程式。 如果您這樣做,編譯器將不會合成一個。 private如果記錄為,請建立您的函式 sealed ,否則進行 protected

您無法覆寫複製方法,也無法建立名為的成員 Clone 。 複製方法的實際名稱是編譯器產生的。

顯示的內建格式

記錄類型具有編譯器產生的 ToString 方法,可顯示公用屬性和欄位的名稱和值。 方法會傳回 ToString 下列格式的字串:

<record type name> { <property name> = <value>, <property name> = <value>, ...}

若為參考型別,則會顯示內容參考之物件的類型名稱,而不是屬性值。 在下列範例中,陣列是參考型別,因此 System.String[] 會顯示,而不是實際的陣列元素值:

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

為了執行這項功能,編譯器會會合成虛擬 PrintMembers 方法和覆 ToString 寫。 覆 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 。 Examples are provided in the PrintMembers formatting in derived records section later in this article.

繼承

記錄可以繼承自另一個記錄。 但是,記錄無法繼承自類別,而且類別無法繼承自記錄。

衍生記錄類型中的位置參數

衍生的記錄會宣告基底記錄主要函式中所有參數的位置參數。 基底記錄會宣告並初始化這些屬性。 衍生的記錄不會隱藏它們,但只會針對其基底記錄中未宣告的參數建立和初始化屬性。

下列範例說明使用位置屬性語法的繼承:

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 }
}

繼承階層中的相等

如果兩個記錄變數相等,則執行時間類型必須相等。 包含變數的類型可能會不同。 下列程式碼範例將說明這一點:

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
}

在此範例中,所有實例都有相同的屬性和相同的屬性值。 但是 student == teacher False ,雖然這兩種類型都是 Person 類型變數,但卻傳回,但 student == student2 True 其中一個是變數, Person 另一個則是 Student 變數。

若要執行這項行為,編譯器會會合成一個 EqualityContract 屬性,以傳回 Type 符合記錄類型的物件。 這可讓相等方法在檢查是否相等時,比較物件的執行時間型別。 如果 a 記錄的基底類型為 object ,則此屬性為 virtual 。 如果基底類型是另一種記錄類型,則此屬性為覆寫。 如果記錄類型為 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 衍生自另一筆記錄的記錄: protected sealed override bool PrintMembers(StringBuilder builder) ;
  • 針對不是 sealed 且衍生自 object 的記錄: protected virtual bool PrintMembers(StringBuilder builder);
  • 針對不是 sealed 且衍生自另一筆記錄的記錄: protected override bool PrintMembers(StringBuilder builder);

以下是取代合成方法的程式碼範例 PrintMembers ,一個用於衍生自 object 的記錄類型,另一個則是衍生自另一個記錄的記錄類型:

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 }
}

解構函式衍生記錄中的行為

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 條件約束。 若要對特定的記錄類型階層進行條件約束,請將條件約束放在基底記錄上,就像是基類一樣。 如需詳細資訊,請參閱 類型參數的條件約束

C# 語言規格

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

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

另請參閱