記錄
注意
本文是功能規格。 規格可作為功能的設計檔。 其中包含建議的規格變更,以及功能設計和開發期間所需的資訊。 這些文章會發佈,直到提議的規格變更完成並併併入目前的ECMA規格為止。
功能規格與已完成實作之間可能有一些差異。 這些差異是在的相關
冠軍議題:https://github.com/dotnet/csharplang/issues/39
此提案會追蹤 C# 9 記錄功能的規格,如 C# 語言設計小組所同意。
記錄的語法如下所示:
record_declaration
: attributes? class_modifier* 'partial'? 'record' identifier type_parameter_list?
parameter_list? record_base? type_parameter_constraints_clause* record_body
;
record_base
: ':' class_type argument_list?
| ':' interface_type_list
| ':' class_type argument_list? ',' interface_type_list
;
record_body
: '{' class_member_declaration* '}' ';'?
| ';'
;
記錄類型是參考型別,類似於類別宣告。 若 record_base
不包含 argument_list
,則提供 record_declaration
parameter_list
是錯誤的。
一個局部記錄的局部類型宣告中最多只能提供一個 parameter_list
。
記錄參數無法使用 ref
、out
或 this
修飾詞(但允許 in
和 params
)。
遺產
除非類別是 object
,否則記錄無法繼承自類別,而且類別無法繼承自記錄。 記錄可以繼承自其他記錄。
記錄類型的成員
除了記錄主體中宣告的成員之外,記錄類型還有額外的合成成員。 若在記錄主體中宣告了具有「比對」簽章的成員,或繼承了具有「比對」簽章的可存取具體非虛擬成員,就不會合成新的成員。 匹配的成員會防止編譯器產生該成員,而不會影響其他合成成員的產生。 如果兩個成員具有相同簽章,或會在繼承案例中被視為「隱藏」,則會被視為相符。 記錄的成員名稱為「Clone」是錯誤的。 記錄的實例欄位具有最上層指標類型是錯誤的。 允許巢狀指標類型,例如指標陣列。
合成成員如下所示:
平等成員
如果記錄衍生自 object
,則記錄類型會包含等同於如下宣告的屬性的合成的唯讀屬性:
Type EqualityContract { get; }
如果記錄類型為 private
,則屬性為 sealed
。 否則,屬性是 virtual
和 protected
。
屬性可以明確宣告。 如果明確宣告不符合預期的方法簽名或存取權限,或該明確宣告不允許在衍生型別中被覆寫,並且記錄型別不是 sealed
,則為錯誤。
如果記錄類型是從基礎記錄類型 Base
衍生的,則該記錄類型會包含一個合成的唯讀屬性,其等同於以下宣告的屬性:
protected override Type EqualityContract { get; }
屬性可以明確宣告。 如果明確宣告不符合預期的方法簽名或存取權限,或該明確宣告不允許在衍生型別中被覆寫,並且記錄型別不是 sealed
,則為錯誤。 如果合成的或明確聲明的屬性未能在記錄類型中覆蓋具有此簽章的屬性 Base
,這將導致錯誤(例如,如果該屬性缺失在 Base
中,或被封閉,或不是虛擬屬性等)。
合成屬性會傳回 typeof(R)
其中 R
是記錄類型。
記錄類型會實作 System.IEquatable<R>
,並包含 Equals(R? other)
的合成強型別多載,其中 R
是記錄類型。
方法是 public
,而除非記錄類型是 virtual
,否則方法是 sealed
。
方法可以明確宣告。 如果明確宣告不符合預期的簽章或可存取性,或明確宣告不允許在衍生型別中覆寫,並且記錄型別不是 sealed
,則視為錯誤。
如果 Equals(R? other)
是使用者定義的(未合成),但 GetHashCode
不是,則會產生警告。
public virtual bool Equals(R? other);
如果下列各項都 Equals(R?)
,則合成 true
會傳回 true
:
-
other
不是null
、 和 - 對於未繼承之記錄類型中的每個實例欄位
fieldN
,System.Collections.Generic.EqualityComparer<TN>.Default.Equals(fieldN, other.fieldN)
的值,其中TN
是字段類型,以及 - 如果有基底記錄類型,則
base.Equals(other)
的值為public virtual bool Equals(Base? other)
的非虛擬呼叫;否則,則為EqualityContract == other.EqualityContract
的值。
記錄類型包括合成的運算子 ==
,這些運算子等效於如下所宣告的運算子:!=
。
public static bool operator==(R? left, R? right)
=> (object)left == right || (left?.Equals(right) ?? false);
public static bool operator!=(R? left, R? right)
=> !(left == right);
Equals
運算符呼叫的 ==
方法,就是上文指定的 Equals(R? other)
方法。
!=
運算符會將任務委派給 ==
運算符。 如果明確宣告運算符,就會發生錯誤。
如果記錄類型衍生自基底記錄類型 Base
,則記錄類型會包含相當於宣告的方法的合成覆寫,如下所示:
public sealed override bool Equals(Base? other);
如果明確宣告覆寫,則為錯誤。 如果方法未覆寫記錄類型中具有相同簽章的方法 Base
,則為錯誤(例如,如果方法在 Base
或密封中遺失,或不是虛擬等)。
合成的覆寫會傳回 Equals((object?)other)
。
記錄類型包含與宣告的方法相等的合成覆寫,如下所示:
public override bool Equals(object? obj);
如果明確宣告覆寫,則為錯誤。 如果方法未覆寫 object.Equals(object? obj)
,就會發生錯誤(例如,因為中繼基底類型等發生陰影)。
合成的覆寫會傳回 Equals(other as R)
,其中 R
是記錄類型。
記錄類型包含與宣告的方法相等的合成覆寫,如下所示:
public override int GetHashCode();
方法可以明確宣告。
如果明確宣告不允許在衍生型別中被覆寫,而記錄類型不是 sealed
,則為錯誤。 如果合成或明確宣告的方法無法覆寫 object.GetHashCode()
,則為錯誤(例如,由於中繼基底類型中的陰影等情況)。
如果明確宣告 Equals(R?)
和 GetHashCode()
之一,但另一個函式並未明確宣告,就會報告警告。
GetHashCode()
的合成覆寫會傳回結合下列值的 int
結果:
- 對於未繼承之記錄類型中的每個實例欄位
fieldN
,System.Collections.Generic.EqualityComparer<TN>.Default.GetHashCode(fieldN)
的值,其中TN
是字段類型,以及 - 如果有基礎記錄類型,則為
base.GetHashCode()
的值;否則,則為System.Collections.Generic.EqualityComparer<System.Type>.Default.GetHashCode(EqualityContract)
的值。
例如,請考慮下列記錄類型:
record R1(T1 P1);
record R2(T1 P1, T2 P2) : R1(P1);
record R3(T1 P1, T2 P2, T3 P3) : R2(P1, P2);
針對這些記錄類型,合成的相等成員會類似:
class R1 : IEquatable<R1>
{
public T1 P1 { get; init; }
protected virtual Type EqualityContract => typeof(R1);
public override bool Equals(object? obj) => Equals(obj as R1);
public virtual bool Equals(R1? other)
{
return !(other is null) &&
EqualityContract == other.EqualityContract &&
EqualityComparer<T1>.Default.Equals(P1, other.P1);
}
public static bool operator==(R1? left, R1? right)
=> (object)left == right || (left?.Equals(right) ?? false);
public static bool operator!=(R1? left, R1? right)
=> !(left == right);
public override int GetHashCode()
{
return HashCode.Combine(EqualityComparer<Type>.Default.GetHashCode(EqualityContract),
EqualityComparer<T1>.Default.GetHashCode(P1));
}
}
class R2 : R1, IEquatable<R2>
{
public T2 P2 { get; init; }
protected override Type EqualityContract => typeof(R2);
public override bool Equals(object? obj) => Equals(obj as R2);
public sealed override bool Equals(R1? other) => Equals((object?)other);
public virtual bool Equals(R2? other)
{
return base.Equals((R1?)other) &&
EqualityComparer<T2>.Default.Equals(P2, other.P2);
}
public static bool operator==(R2? left, R2? right)
=> (object)left == right || (left?.Equals(right) ?? false);
public static bool operator!=(R2? left, R2? right)
=> !(left == right);
public override int GetHashCode()
{
return HashCode.Combine(base.GetHashCode(),
EqualityComparer<T2>.Default.GetHashCode(P2));
}
}
class R3 : R2, IEquatable<R3>
{
public T3 P3 { get; init; }
protected override Type EqualityContract => typeof(R3);
public override bool Equals(object? obj) => Equals(obj as R3);
public sealed override bool Equals(R2? other) => Equals((object?)other);
public virtual bool Equals(R3? other)
{
return base.Equals((R2?)other) &&
EqualityComparer<T3>.Default.Equals(P3, other.P3);
}
public static bool operator==(R3? left, R3? right)
=> (object)left == right || (left?.Equals(right) ?? false);
public static bool operator!=(R3? left, R3? right)
=> !(left == right);
public override int GetHashCode()
{
return HashCode.Combine(base.GetHashCode(),
EqualityComparer<T3>.Default.GetHashCode(P3));
}
}
複製成員和克隆成員
記錄類型包含兩個可複製的成員:
- 採用記錄類型單一自變數的建構函式。 它稱為「複製建構函式」。
- 具有編譯器保留名稱的合成無參數公開實例「clone」方法
複製建構函式的目的是將狀態從 參數複製到正在建立的新實例。 此建構函式不會執行記錄宣告中出現的任何實例字段/屬性初始化表達式。 如果未明確宣告建構函式,編譯程式將會合成建構函式。 如果記錄已密封,建構函式將會是私用的,否則會受到保護。 除非該記錄是封裝的,否則明確宣告的複製建構函式必須是公用或受保護的。 第一個建構函式必須執行的動作是:如果該記錄繼承自物件,則需呼叫物件的無參數建構函式,否則需呼叫基底的複製建構函式。 如果使用者定義複製建構函式使用不符合此需求的隱含或明確建構函式初始化表達式,就會回報錯誤。 叫用基底複製建構函式之後,合成複製建構函式會針對記錄類型內隱含或明確宣告的所有實例字段複製值。 複製建構函式的唯一存在,無論是明確或隱含,都不會防止自動加入預設實例建構函式。
如果在基底記錄中存在虛擬的「clone」方法,那麼合成的「clone」方法就會覆寫它,並且該方法的傳回類型為當前包含的類型。 如果基底記錄複製方法已密封,就會產生錯誤。 如果基底記錄中沒有虛擬 「clone」 方法,則 clone 方法的傳回類型是包含的類型,而且方法為虛擬,除非記錄已密封或抽象。 如果包含的記錄是抽象的,合成複製方法也是抽象的。 如果 「clone」 方法不是抽象的,它會傳回對複製建構函式的呼叫結果。
列印成員:PrintMembers 和 ToString 方法
如果記錄是從 object
衍生的,則該記錄會包含與以下宣告的方法等效的合成的方法:
bool PrintMembers(System.Text.StringBuilder builder);
如果記錄類型是 private
,方法是 sealed
。 否則,方法是 virtual
和 protected
。
方法:
- 如果方法存在,且記錄具有可列印的成員,則會呼叫 方法
System.Runtime.CompilerServices.RuntimeHelpers.EnsureSufficientExecutionStack()
。 - 針對每一筆記錄的可列印成員(非靜態公用欄位和可讀取的屬性成員),將該成員的名稱附加上 " = ",然後,將成員的值用 ", " 分隔後接上去。
- 如果記錄包含可以列印的成員,則返回 true。
對於具有實值型別的成員,我們會使用目標平臺最有效率的方法,將其值轉換成字串表示法。 目前這表示,在傳遞至 ToString
之前,需呼叫 StringBuilder.Append
。
如果記錄類型衍生自基底記錄 Base
,則記錄會包含相當於宣告的方法的合成覆寫,如下所示:
protected override bool PrintMembers(StringBuilder builder);
如果記錄沒有可列印的成員,此方法會使用一個自變數呼叫基底 PrintMembers
方法(其 builder
參數),並傳回結果。
否則,方法:
- 以一個引數呼叫基礎
PrintMembers
方法(其builder
參數), - 如果
PrintMembers
方法傳回 true,請將 “, ” 附加至產生器, - 針對每一筆記錄的可列印成員,附加該成員的名稱,後接 " = ",然後是成員的值:
this.member
(或對於值類型為this.member.ToString()
),並用 "," 分隔。 - 回傳 true。
PrintMembers
方法可以明確宣告。
如果明確宣告不符合預期的方法簽名或存取權限,或該明確宣告不允許在衍生型別中被覆寫,並且記錄型別不是 sealed
,則為錯誤。
記錄包含相當於宣告的方法的合成方法,如下所示:
public override string ToString();
方法可以明確宣告。 如果明確宣告不符合預期的方法簽名或存取權限,或該明確宣告不允許在衍生型別中被覆寫,並且記錄型別不是 sealed
,則為錯誤。 如果合成或明確宣告的方法無法覆寫 object.ToString()
,則為錯誤(例如,由於中繼基底類型中的陰影等情況)。
合成方法:
- 建立
StringBuilder
實例, - 將記錄名稱附加至產生器,後面接著 “ {”,
- 會叫用記錄的
PrintMembers
方法給它產生器,後面接著 “”,如果傳回 true, - appends “}”,
- 會傳回具有
builder.ToString()
的建構器內容。
例如,請考慮下列記錄類型:
record R1(T1 P1);
record R2(T1 P1, T2 P2, T3 P3) : R1(P1);
針對這些記錄類型,合成的列印元件會是類似的:
class R1 : IEquatable<R1>
{
public T1 P1 { get; init; }
protected virtual bool PrintMembers(StringBuilder builder)
{
builder.Append(nameof(P1));
builder.Append(" = ");
builder.Append(this.P1); // or builder.Append(this.P1.ToString()); if T1 is 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();
}
}
class R2 : R1, IEquatable<R2>
{
public T2 P2 { get; init; }
public T3 P3 { get; init; }
protected override bool PrintMembers(StringBuilder builder)
{
if (base.PrintMembers(builder))
builder.Append(", ");
builder.Append(nameof(P2));
builder.Append(" = ");
builder.Append(this.P2); // or builder.Append(this.P2); if T2 is a value type
builder.Append(", ");
builder.Append(nameof(P3));
builder.Append(" = ");
builder.Append(this.P3); // or builder.Append(this.P3); if T3 is a value type
return true;
}
public override string ToString()
{
var builder = new StringBuilder();
builder.Append(nameof(R2));
builder.Append(" { ");
if (PrintMembers(builder))
builder.Append(" ");
builder.Append("}");
return builder.ToString();
}
}
位置記錄成員
除了上述成員之外,具有參數清單的記錄(「位置記錄」)會合成與上述成員相同條件的其他成員。
主要建構函式
記錄類型具有一個公共建構函式,其原型對應於類型聲明中的值參數。 這稱為類型的主要建構函式,而且如果存在,則會隱藏隱含宣告的預設類別建構函式。 在類別中,若已有具有相同簽章的建構函式,則不應再定義主要建構函式,這是一個錯誤。
執行時,主要構造函式
執行出現在類別主體中的實例初始化表達式
使用
record_base
子句中提供的自變數叫用基類建構函式,如果有的話
如果記錄具有主要建構函式,則「複製建構函式」以外的任何使用者定義建構函式都必須有明確的 this
建構函式初始化表達式。
主要建構函式和記錄成員的參數處於 argument_list
子句的 record_base
範圍內,以及實例欄位或屬性的初始化表達式中。 實例成員在這些位置將會造成錯誤(就像在一般建構函式初始化中,實例成員在範圍內卻會使用錯誤一樣),但主要建構函式的參數將會處於範圍內、可使用,並且會遮蔽成員。 靜態成員也可以使用,類似於基底呼叫和初始化表達式在現今一般建構函式中的運作方式。
如果未讀取主要建構函式的參數,則會產生警告。
在 argument_list
中宣告的表達式變數位於 argument_list
的範圍內。 套用與一般建構函式初始化的參數列表中相同的遮蔽規則。
性能
對於記錄類型宣告中的每個參數,都會產生一個相應的公用屬性成員,該成員的名稱和類型取自於該參數的值宣告。
備案:
- 會建立公開的
get
和init
自動屬性(請參閱單獨的init
存取子規範)。 繼承的abstract
屬性已被相符類型覆寫。 如果繼承的屬性沒有public
可覆寫get
和init
存取子,則為錯誤。 隱藏繼承屬性會導致錯誤。
auto 屬性會初始化為對應主要建構函式參數的值。 屬性可以套用到合成的自動屬性及其基礎支援欄位,方法是使用property:
或field:
作為目標,將屬性語法應用到對應的記錄參數。
解構
至少有一個參數的位置記錄會合成一個名為 Deconstruct 的公用 void 傳回值的實例方法,該方法為主要建構函式宣告的每個參數使用 out 參數宣告。
Deconstruct
方法的每個參數都有與主要建構函式宣告之對應參數相同的類型。 方法的主體會指派給 Deconstruct
方法的每個參數,也就是相同名稱的實例屬性值。
方法可以明確宣告。 如果明確宣告不符合預期的函式簽章、可存取性,或宣告為靜態,則為錯誤。
下列範例顯示一個名為 R
的位置記錄,其編譯器合成的 Deconstruct
方法,以及其用法:
public record R(int P1, string P2 = "xyz")
{
public void Deconstruct(out int P1, out string P2)
{
P1 = this.P1;
P2 = this.P2;
}
}
class Program
{
static void Main()
{
R r = new R(12);
(int p1, string p2) = r;
Console.WriteLine($"p1: {p1}, p2: {p2}");
}
}
with
表示式
with
表示式是使用下列語法的新的表示式。
with_expression
: switch_expression
| switch_expression 'with' '{' member_initializer_list? '}'
;
member_initializer_list
: member_initializer (',' member_initializer)*
;
member_initializer
: identifier '=' expression
;
不允許使用 with
表達式做為語句。
with
表達式允許「非破壞性突變」,其設計目的是要產生接收者表達式的複本,並修改 member_initializer_list
中的指派。
有效的 with
表示式具有具有非 void 類型的接收者。 接收者類型必須是記錄。
with
表示式右側是一個 member_initializer_list
,具有 標識元的指派序列,必須是接收者類型的可存取實例字段或屬性。
首先,會叫用接收者的「複製」方法(上面指定),並將其結果轉換成接收者的型別。 然後,處理每個 member_initializer
的方式與轉換結果的欄位或屬性存取權指派相同。 指派會以語匯順序處理。