共用方式為


記錄結構

注意

本文是功能規格。 規格可作為功能的設計檔。 其中包含建議的規格變更,以及功能設計和開發期間所需的資訊。 這些文章會發佈,直到提議的規格變更完成並併併入目前的ECMA規格為止。

功能規格與已完成實作之間可能有一些差異。 這些差異是在 的相關語言設計會議(LDM)注意事項中擷取的。

您可以在介紹 規格的一文中,詳細了解將功能規範納入 C# 語言標準的過程

冠軍問題:https://github.com/dotnet/csharplang/issues/4334

記錄結構的語法如下所示:

record_struct_declaration
    : attributes? struct_modifier* 'partial'? 'record' 'struct' identifier type_parameter_list?
      parameter_list? struct_interfaces? type_parameter_constraints_clause* record_struct_body
    ;

record_struct_body
    : struct_body
    | ';'
    ;

記錄結構類型是實值型別,就像其他結構類型一樣。 它們會隱式繼承自類別 System.ValueType。 記錄結構的修飾詞和成員受限於結構的限制(類型的可見性、成員上的修飾詞、base(...) 實例構造函式的初始化器、建構函式中 this 的確定賦值、解構函式...)。記錄結構也會遵循與無參數實例建構函式和字段初始化器相同的規則,但此文件假設我們會普遍解除結構的這些限制。

請參閱 •16.4.9 請參閱 無參數結構建構函式 規格。

記錄結構無法使用 ref 修飾詞。

部分記錄結構的一個部分類型宣告最多可以提供 parameter_listparameter_list 可能是空的。

記錄結構參數無法使用 refoutthis 修飾詞(但允許 inparams)。

記錄結構的成員

除了記錄結構主體中宣告的成員之外,記錄結構類型還有額外的合成成員。 成員會被合成,除非在記錄結構體中宣告了一個具有「匹配」簽章的成員,或者繼承了一個具有「匹配」簽章、可存取的具體非虛擬成員。 如果兩個成員被認為具有相同的簽章,或在繼承情境中被視為「隱藏」,那麼它們就會被認為是相符的。 請參閱簽章和多載 §7.6。 記錄結構中,將成員命名為「Clone」是一種錯誤。

記錄結構的實例欄位具有不安全的類型是錯誤的。

不允許記錄結構宣告解構函式。

合成成員如下所示:

平等委員會成員

合成的等號成員與記錄類別類似(此類型的Equalsobject 類型的 Equals、此類型的 ==!= 運算元),
除了缺少 EqualityContract、空值檢查或是繼承。

記錄結構實作 System.IEquatable<R>,並包含 Equals(R other) 的合成強型別多載,其中 R 是記錄結構。 方法 public。 方法可以明確宣告。 如果明確宣告不符合預期的簽章或存取性,就會發生錯誤。

如果 Equals(R other) 是使用者定義的(未合成),但 GetHashCode 不是,則會產生警告。

public readonly bool Equals(R other);

如果記錄 System.Collections.Generic.EqualityComparer<TN>.Default.Equals(fieldN, other.fieldN) 中的每個實例欄位 fieldNTN 是欄位類型是 true,則合成 Equals(R) 會傳回 true

記錄結構包括相當於以下所宣告的運算符的合成 ==!= 運算子:

public static bool operator==(R r1, R r2)
    => r1.Equals(r2);
public static bool operator!=(R r1, R r2)
    => !(r1 == r2);

== 運算符所呼叫 Equals 方法是上面指定的 Equals(R other) 方法。 != 運算符會委派給 == 運算符。 如果明確宣告運算符,就會發生錯誤。

記錄結構包含相當於宣告的方法的合成覆寫,如下所示:

public override readonly bool Equals(object? obj);

如果明確宣告覆寫,則為錯誤。 合成的覆寫會傳回 other is R temp && Equals(temp),其中 R 是記錄結構。

記錄結構包含相當於宣告的方法的合成覆寫,如下所示:

public override readonly int GetHashCode();

方法可以明確宣告。

如果明確宣告 Equals(R)GetHashCode() 之一,但另一個方法並不明確,就會報告警告。

GetHashCode() 的合成覆寫會返回一個結合每個實例字段 fieldNSystem.Collections.Generic.EqualityComparer<TN>.Default.GetHashCode(fieldN) 值所得到的 int 結果,其中 TNfieldN的類型。

例如,請考慮下列記錄結構:

record struct R1(T1 P1, T2 P2);

針對此記錄結構,合成的相等比較成員將類似:

struct R1 : IEquatable<R1>
{
    public T1 P1 { get; set; }
    public T2 P2 { get; set; }
    public override bool Equals(object? obj) => obj is R1 temp && Equals(temp);
    public bool Equals(R1 other)
    {
        return
            EqualityComparer<T1>.Default.Equals(P1, other.P1) &&
            EqualityComparer<T2>.Default.Equals(P2, other.P2);
    }
    public static bool operator==(R1 r1, R1 r2)
        => r1.Equals(r2);
    public static bool operator!=(R1 r1, R1 r2)
        => !(r1 == r2);    
    public override int GetHashCode()
    {
        return Combine(
            EqualityComparer<T1>.Default.GetHashCode(P1),
            EqualityComparer<T2>.Default.GetHashCode(P2));
    }
}

列印成員:PrintMembers 和 ToString 方法

記錄結構包含相當於宣告的方法的合成方法,如下所示:

private bool PrintMembers(System.Text.StringBuilder builder);

方法會執行下列動作:

  1. 針對每個記錄結構的可列印成員(非靜態公用欄位和可讀取的屬性成員),附加該成員的名稱,後面接著“ = ”,然後是該成員的值,以“, ”分隔。
  2. 如果記錄結構具有可列印的成員,則傳回 true。

對於具有實值型別的成員,我們會使用目標平臺最有效率的方法,將其值轉換成字串表示法。 目前這意味著在傳遞至 StringBuilder.Append之前呼叫 ToString

如果記錄的可列印成員不包含具有非readonlyget 存取子的可讀取屬性,則合成的 PrintMembersreadonly。 要使用 PrintMembers 方法 readonly,記錄的欄位不需要設為 readonly

PrintMembers 方法可以明確宣告。 如果明確宣告不符合預期的簽章或輔助功能,就會發生錯誤。

記錄結構包含相當於宣告的方法的合成方法,如下所示:

public override string ToString();

如果記錄結構的 PrintMembers 方法是 readonly,則合成 ToString() 方法是 readonly

方法可以明確宣告。 如果明確宣告不符合預期的簽章或可見性,則為錯誤。

合成方法:

  1. 建立 StringBuilder 實例,
  2. 將記錄結構名稱附加至產生器,後面接著 “ {”,
  3. 會叫用記錄結構 PrintMembers 方法,為它提供產生器,後面接著 “”,如果傳回 true,
  4. appends “}”,
  5. 會傳回包含 builder.ToString()的構建器內容。

例如,請考慮下列記錄結構:

record struct R1(T1 P1, T2 P2);

針對此記錄結構,自動生成的列印成員可能會是這樣:

struct R1 : IEquatable<R1>
{
    public T1 P1 { get; set; }
    public T2 P2 { get; set; }

    private bool PrintMembers(StringBuilder builder)
    {
        builder.Append(nameof(P1));
        builder.Append(" = ");
        builder.Append(this.P1); // or builder.Append(this.P1.ToString()); if P1 has a value type
        builder.Append(", ");

        builder.Append(nameof(P2));
        builder.Append(" = ");
        builder.Append(this.P2); // or builder.Append(this.P2.ToString()); if P2 has a value type

        return true;
    }

    public override string ToString()
    {
        var builder = new StringBuilder();
        builder.Append(nameof(R1));
        builder.Append(" { ");

        if (PrintMembers(builder))
            builder.Append(" ");

        builder.Append("}");
        return builder.ToString();
    }
}

位置記錄結構成員

除了上述成員之外,使用參數清單的紀錄結構體(「位置記錄」)會自動生成具有與上述成員相同條件的額外成員。

主要建構函式

記錄結構具有公用建構函式,其簽章會對應至類型宣告的值參數。 這稱為類型的主要建構函式。 在結構中擁有一個主要建構函式及一個相同簽章的建構函式是錯誤的。 如果類型宣告不包含參數清單,則不會產生任何主要建構函式。

record struct R1
{
    public R1() { } // ok
}

record struct R2()
{
    public R2() { } // error: 'R2' already defines constructor with same parameter types
}

允許記錄結構的實例欄位宣告包含變數初始化器。 如果沒有主要建構函式,實例初始化表達式會執行為無參數建構函式的一部分。 否則,在運行時間,主要建構函式會執行出現在 record-struct-body 中的實例初始化運算式。

如果記錄結構具有主要建構函式,則任何使用者定義的建構函式都必須有明確 this 建構函式初始化表達式,以呼叫主要建構函式或明確宣告的建構函式。

主要建構函式的參數和記錄結構的成員在實例欄位或屬性初始化器的範圍內。 實例成員在這些位置會造成錯誤(就像目前在一般建構函式的初始化中實例成員的使用一樣,但使用它們會是錯誤的),然而,主要建構函式的參數則在作用域內並可供使用,並且會覆蓋掉成員。 靜態成員也可被使用。

如果未讀取主要建構函式的參數,則會產生警告。

結構實例建構函式的明確指派規則會套用至記錄結構的主要建構函式。 例如,下列是錯誤:

record struct Pos(int X) // definite assignment error in primary constructor
{
    private int x;
    public int X { get { return x; } set { x = value; } } = X;
}

性能

對於記錄結構宣告的每個記錄結構參數,會有一個對應的公用屬性成員,其名稱和類型取自 value 參數宣告。

針對記錄結構:

  • 如果記錄結構具有 readonly 修飾詞、getset,則會建立公用 getinit 自動屬性。 這兩種類型的 set 存取子(setinit)都會被視為「符合」。 因此,使用者可以宣告初始化僅限屬性來取代合成可變屬性。 已繼承的相符類型 abstract 屬性被覆寫。 如果記錄結構具有具有預期名稱和類型的實例欄位,則不會建立任何自動屬性。 如果繼承的屬性沒有 publicgetset/init 存取器,就會發生錯誤。 如果隱藏繼承的屬性或欄位,就會發生錯誤。
    auto 屬性會初始化為對應主要建構函式參數的值。 屬性可以套用至合成的自動屬性及其支援欄位,方法是使用 property:field: 目標,以語法方式套用至對應記錄結構參數的屬性。

解構

具有至少一個參數的位置紀錄結構會生成一個名為 Deconstruct 的公用、返回 void 的實例方法,並為主要構造函式宣告的每個參數提供 out 參數宣告。 解構方法的每個參數都有與主要建構函式宣告之對應參數相同的類型。 方法的主體會將解構方法的每個參數指派給實例成員存取相同名稱成員的值。 如果在主體中存取的實體成員不包含具有非readonlyget 存取子的屬性,那麼合成的 Deconstruct 方法會是 readonly。 方法可以明確宣告。 如果明確宣告不符合預期的簽章或存取性,或為靜態,則為錯誤。

允許在結構體上使用 with 表達式

現在,with 表示式中的接收者具有結構類型是有效的。

with 表示式右側是一個 member_initializer_list,具有 標識元的指派序列,必須是接收者類型的可存取實例字段或屬性。

對於結構類型的接收者,會先複製接收者,然後每個 member_initializer 會以與轉換結果之欄位或屬性存取權指派相同的方式處理。 指派會以語匯順序處理。

記錄的改善

允許 record class

記錄類型的現有語法允許 record classrecord相同意義:

record_declaration
    : attributes? class_modifier* 'partial'? 'record' 'class'? identifier type_parameter_list?
      parameter_list? record_base? type_parameter_constraints_clause* record_body
    ;

允許使用者定義的位置成員成為欄位

請參閱 https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-10-05.md#changing-the-member-type-of-a-primary-constructor-parameter

如果記錄具有或繼承具有預期名稱和類型的實例欄位,則不會建立任何自動屬性。

允許結構中的無參數建構函式和成員初始化表達式

請參閱 無參數結構建構函式 規格。

未解決問題

  • 如何辨識元數據中的記錄結構? (我們沒有無法言喻的克隆方法可以運用...)

已回答

  • 確認我們想要保留 PrintMembers 設計(個別傳回 bool的方法)(答案:是)
  • 確認我們不會允許 record ref structIEquatable<RefStruct> 和 ref 字段的問題) (答案: 是)
  • 確認是否實作相等成員。 替代方案是合成的 bool Equals(R other)bool Equals(object? other) 和運算符全都只委派給 ValueType.Equals。 (答:是的)
  • 確認我們想要在當存在主要建構函式時允許欄位初始化器。 我們是否也想允許無參數結構建構函式,同時解決 Activator 問題的時候(顯然已經修正)? (答:是的,應在LDM中檢閱更新的規格)
  • 我們要講多少關於Combine方法? (答:盡可能少)
  • 我們應該禁止具有複製建構函式簽章的使用者定義建構函式嗎? (答:否,記錄結構規格中沒有複製建構函式的概念)
  • 確認我們想要不允許名為 「Clone」 的成員。 (答:正確)
  • 仔細檢查合成 Equals 邏輯在功能上是否等同於運行時的實作(例如 float.NaN) (確認答覆:已在 LDM 確認)
  • 欄位或屬性目標屬性可以放在位置參數清單中嗎? (答:是的,與記錄類別相同)
  • with 是關於泛型的嗎? (答:不在 C# 10 的範疇內)
  • 應該 GetHashCode 包含類型自身的哈希值,以便在 record struct S1;record struct S2;之間獲得不同的值? (答:否)