Rekordy
Notatka
Ten artykuł jest specyfikacją funkcji. Specyfikacja służy jako dokument projektowy dla funkcji. Zawiera proponowane zmiany specyfikacji wraz z informacjami wymaganymi podczas projektowania i opracowywania funkcji. Te artykuły są publikowane do momentu sfinalizowania proponowanych zmian specyfikacji i włączenia ich do obecnej specyfikacji ECMA.
Mogą wystąpić pewne rozbieżności między specyfikacją funkcji a ukończoną implementacją. Te różnice są przechwytywane w odpowiednich spotkania projektowego języka (LDM).
Więcej informacji na temat procesu wdrażania specyfikacji funkcji można znaleźć w standardzie języka C# w artykule dotyczącym specyfikacji .
Problem z kategorią "Mistrz": https://github.com/dotnet/csharplang/issues/39
Propozycja dotyczy specyfikacji funkcji rekordów języka C# 9, uzgodnioną przez zespół projektowy języka C#.
Składnia rekordu jest następująca:
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* '}' ';'?
| ';'
;
Typy rekordów to typy odwołań podobne do deklaracji klasy. Jest błędem, jeśli rekord dostarcza record_base
argument_list
, gdy record_declaration
nie zawiera parameter_list
.
Co najwyżej jedna częściowa deklaracja typu rekordu częściowego może dostarczyć parameter_list
.
Parametry rekordu nie mogą używać ref
, modyfikatorów out
lub this
(ale dozwolone są in
i params
).
Dziedziczenie
Rekordy nie mogą dziedziczyć z klas, chyba że klasa jest object
, a klasy nie mogą dziedziczyć z rekordów. Rekordy mogą dziedziczyć z innych rekordów.
Członkowie typu rekordu
Oprócz członków zadeklarowanych w treści rekordu typ rekordu ma dodatkowe syntetyzowane elementy członkowskie. Składowe są syntetyzowane, chyba że członek z "pasującą" sygnaturą jest zadeklarowany w treści rekordu lub dziedziczony jest dostępny konkretny niewirtualny członek z "pasującą" sygnaturą. Pasujący element członkowski uniemożliwia kompilatorowi generowanie tego elementu członkowskiego, a nie żadnych innych syntetyzowanych elementów członkowskich. Dwa elementy członkowskie są uważane za zgodne, jeśli mają ten sam podpis lub będą uznawane za "przesłanianie" w scenariuszu dziedziczenia. Błąd polega na tym, że członek rekordu ma nazwę "Clone". Błędem jest, gdy pole wystąpienia rekordu ma typ wskaźnika najwyższego poziomu. Dozwolony jest typ zagnieżdżonego wskaźnika, taki jak tablica wskaźników.
Syntetyzowane składowe są następujące:
Członkowie na rzecz równości
Jeśli rekord pochodzi z object
, typ rekordu zawiera syntetyzowaną właściwość readonly równoważną właściwości zadeklarowanej w następujący sposób:
Type EqualityContract { get; }
Ta właściwość to private
, jeśli typ rekordu to sealed
. W przeciwnym razie właściwości są virtual
i protected
.
Właściwość można zadeklarować jawnie. Jest to błąd, jeśli jawna deklaracja nie pasuje do oczekiwanego podpisu lub dostępności, lub jeśli jawna deklaracja nie zezwala na zastępowanie jej w typie pochodnym, a rekord nie jest sealed
.
Jeśli typ rekordu jest pochodną typu rekordu podstawowego Base
, typ rekordu zawiera syntetyzowaną właściwość tylko do odczytu, równoważną właściwości zadeklarowanej w następujący sposób:
protected override Type EqualityContract { get; }
Właściwość można zadeklarować jawnie. Jest to błąd, jeśli jawna deklaracja nie pasuje do oczekiwanego podpisu lub dostępności, lub jeśli jawna deklaracja nie zezwala na zastępowanie jej w typie pochodnym, a rekord nie jest sealed
. Występuje błąd, jeżeli zsyntetyzowana lub jawnie zadeklarowana właściwość nie zastępuje właściwości z tym podpisem w typie rekordu Base
(na przykład, jeżeli brakuje właściwości w Base
, lub jest zapieczętowana, lub niewirtualna itp.).
Syntetyzowana właściwość zwraca typeof(R)
, gdzie R
jest typem rekordu.
Typ rekordu implementuje System.IEquatable<R>
i zawiera syntetyzowane silnie typizowane przeciążenie Equals(R? other)
gdzie R
jest typem rekordu.
Metoda jest public
, a metoda jest virtual
, chyba że typ rekordu jest sealed
.
Metodę można zadeklarować jawnie. Jest to błąd, jeśli jawna deklaracja nie pasuje do oczekiwanego podpisu lub dostępności, lub jeśli jawna deklaracja nie pozwala na jej zastępowanie w typie pochodnym, a typ rekordu nie jest sealed
.
Jeśli Equals(R? other)
jest definiowana przez użytkownika (nie jest syntetyzowana), ale GetHashCode
nie, zostanie wygenerowane ostrzeżenie.
public virtual bool Equals(R? other);
Syntetyzowana Equals(R?)
zwraca true
tylko wtedy, gdy wszystkie z poniższych true
są spełnione:
-
other
nie jestnull
i - Dla każdego pola wystąpienia
fieldN
w typie rekordu, który nie jest dziedziczony, wartośćSystem.Collections.Generic.EqualityComparer<TN>.Default.Equals(fieldN, other.fieldN)
, gdzieTN
jest typem pola i - Jeśli istnieje typ rekordu podstawowego, wartość
base.Equals(other)
(wywołanie niewirtualne skierowane dopublic virtual bool Equals(Base? other)
); w przeciwnym razie jest to wartośćEqualityContract == other.EqualityContract
.
Typ rekordu obejmuje syntetyzowane operatory ==
i !=
równoważne operatorom zadeklarowanych w następujący sposób:
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);
Metoda Equals
wywoływana przez operator ==
jest metodą Equals(R? other)
określoną powyżej. Operator !=
przechodzi do operatora ==
. Jest to błąd, w przypadku gdy operatory są zadeklarowane jawnie.
Jeśli typ rekordu pochodzi z typu rekordu podstawowego Base
, typ rekordu zawiera syntetyzowane zastąpienie równoważne metodzie zadeklarowanej w następujący sposób:
public sealed override bool Equals(Base? other);
Jest to błąd, gdy zastąpienie zostaje zadeklarowane jawnie. Jest to błąd, jeśli metoda nie zastępuje metody z tym samym podpisem w typie rekordu Base
(na przykład jeśli brakuje metody w Base
lub zapieczętowanej, lub nie wirtualnej itp.).
Syntetyzowane przełączenie zwraca Equals((object?)other)
.
Typ rekordu zawiera syntetyzowane zastąpienie równoważne metodzie zadeklarowanej w następujący sposób:
public override bool Equals(object? obj);
Jest to błąd, gdy zastąpienie zostaje zadeklarowane jawnie. Jest to błąd, jeśli metoda nie zastępuje object.Equals(object? obj)
(na przykład z powodu zaciemnienia w pośrednich typach bazowych itp.).
Syntezowane przesłonięcia zwracają Equals(other as R)
, gdzie R
jest typem rekordu.
Typ rekordu zawiera syntetyzowane zastąpienie równoważne metodzie zadeklarowanej w następujący sposób:
public override int GetHashCode();
Metodę można zadeklarować jawnie.
Jest to błąd, jeśli jawna deklaracja nie zezwala na zastępowanie go w typie pochodnym, a typ rekordu nie jest sealed
. Jest to błąd, jeśli metoda syntetyzowana lub jawnie zadeklarowana nie zastępuje object.GetHashCode()
(na przykład z powodu cieniowania w typach baz pośrednich itp.).
Ostrzeżenie jest zgłaszane, jeśli jedna z metod Equals(R?)
i GetHashCode()
jest jawnie zadeklarowana, ale druga metoda nie jest zadeklarowana jawnie.
Syntetyzowane przesłonięcie GetHashCode()
zwraca wynik int
łączący następujące wartości:
- Dla każdego pola wystąpienia
fieldN
w typie rekordu, który nie jest dziedziczony, wartośćSystem.Collections.Generic.EqualityComparer<TN>.Default.GetHashCode(fieldN)
, gdzieTN
jest typem pola i - Jeśli istnieje typ rekordu podstawowego, wartość
base.GetHashCode()
; w przeciwnym razie wartośćSystem.Collections.Generic.EqualityComparer<System.Type>.Default.GetHashCode(EqualityContract)
.
Rozważmy na przykład następujące typy rekordów:
record R1(T1 P1);
record R2(T1 P1, T2 P2) : R1(P1);
record R3(T1 P1, T2 P2, T3 P3) : R2(P1, P2);
W przypadku tych typów rekordów, zsyntetyzowane składniki równości będą wyglądać mniej więcej tak:
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));
}
}
Kopiowanie i klonowanie członków
Typ rekordu zawiera dwóch członków kopiujących.
- Konstruktor przyjmujący jeden argument typu rekordu. Jest on określany jako "konstruktor kopiujący".
- Syntetyzowana metoda publiczna "clone" dla instancji bez parametrów z nazwą zarezerwowaną przez kompilator
Celem konstruktora kopiowania jest skopiowanie stanu z parametru do tworzonego nowego wystąpienia. Ten konstruktor nie uruchamia żadnych inicjatorów pól/właściwości instancji znajdujących się w deklaracji rekordu. Jeśli konstruktor nie zostanie jawnie zadeklarowany, konstruktor zostanie zsyntetyzowany przez kompilator. Jeśli rekord jest zapieczętowany, konstruktor będzie prywatny, w przeciwnym razie będzie chroniony. Jawnie zadeklarowany konstruktor kopiujący musi być publiczny lub chroniony, chyba że rekord jest zamknięty. Pierwszą rzeczą, którą musi wykonać konstruktor, jest wywołanie konstruktora kopii bazy lub konstruktora obiektu bez parametrów, jeśli rekord dziedziczy z obiektu. Zgłaszany jest błąd, jeśli konstruktor kopiujący zdefiniowany przez użytkownika używa niejawnego lub jawnego inicjatora konstruktora, który nie spełnia tego wymagania. Po wywołaniu konstruktora kopii bazowej syntetyzowany konstruktor kopiujący kopiuje wartości dla wszystkich pól wystąpień niejawnie lub jawnie zadeklarowanych w ramach typu rekordu. Jedyna obecność konstruktora kopiowania, niezależnie od tego, czy jest jawna, czy niejawna, nie zapobiega automatycznemu dodawaniu domyślnego konstruktora wystąpienia.
Jeśli wirtualna metoda "clone" jest obecna w rekordzie podstawowym, zostaje zastąpiona przez syntetyzowaną metodę "clone," a zwracany typ tej metody to bieżący typ, który zawiera tę metodę. Błąd jest generowany, jeśli metoda klonowania rekordu podstawowego jest zapieczętowana. Jeśli wirtualna metoda "clone" nie istnieje w rekordzie podstawowym, zwracany typ metody klonowania jest typem zawierającym, a metoda jest wirtualna, chyba że rekord jest zapieczętowany lub abstrakcyjny. Jeśli zawierający rekord jest abstrakcyjny, syntetyzowana metoda klonowania jest również abstrakcyjna. Jeśli metoda "clone" nie jest abstrakcyjna, zwraca wynik wywołania konstruktora kopiowania.
Członkowie drukowania: metody PrintMembers i ToString
Jeśli rekord pochodzi z object
, rekord zawiera syntetyzowana metoda równoważna metodzie zadeklarowanej w następujący sposób:
bool PrintMembers(System.Text.StringBuilder builder);
Metoda jest private
, jeśli typ rekordu jest sealed
. W innym przypadku metoda to virtual
i protected
.
Metoda:
- wywołuje metodę
System.Runtime.CompilerServices.RuntimeHelpers.EnsureSufficientExecutionStack()
, jeśli metoda jest obecna, a rekord zawiera drukowalne elementy. - dla każdego z członków rekordu, którzy mogą być wydrukowani (niestatyczne publiczne pola i możliwe do odczytu członkowie właściwości), dołącza nazwę tego członka, po której następuje " = ", a następnie wartość członka oddzielona znakiem ", ".
- zwraca wartość true, jeśli rekord zawiera elementy członkowskie z możliwością drukowania.
W przypadku elementu członkowskiego, który ma typ wartości, przekonwertujemy jego wartość na reprezentację ciągu przy użyciu najbardziej wydajnej metody dostępnej dla platformy docelowej. Obecnie oznacza to wywołanie ToString
przed przekazaniem do StringBuilder.Append
.
Jeśli typ rekordu pochodzi z rekordu podstawowego Base
, rekord zawiera syntetyzowane zastąpienie równoważne metodzie zadeklarowanej w następujący sposób:
protected override bool PrintMembers(StringBuilder builder);
Jeśli rekord nie ma drukowalnych elementów członkowskich, metoda wywołuje metodę base PrintMembers
z jednym argumentem (jej parametrem builder
) i zwraca wynik.
W przeciwnym razie metoda:
- wywołuje metodę base
PrintMembers
z jednym argumentem (parametrembuilder
), - jeśli metoda
PrintMembers
zwróciła wartość true, dołącz ", " do konstruktora, - dla każdego z członków rekordu możliwych do wydrukowania, dodaje nazwę tego członka, po której następuje znak " = " i wartość członka:
this.member
(lubthis.member.ToString()
dla typów wartości), oddzielone ciągiem znaków ", ", - zwraca wartość true.
Metodę PrintMembers
można zadeklarować jawnie.
Jest to błąd, jeśli jawna deklaracja nie pasuje do oczekiwanego podpisu lub dostępności, lub jeśli jawna deklaracja nie zezwala na zastępowanie jej w typie pochodnym, a rekord nie jest sealed
.
Rekord zawiera metodę syntetyzowana równoważną metodzie zadeklarowanej w następujący sposób:
public override string ToString();
Metodę można zadeklarować jawnie. Jest to błąd, jeśli jawna deklaracja nie pasuje do oczekiwanego podpisu lub dostępności, lub jeśli jawna deklaracja nie zezwala na zastępowanie jej w typie pochodnym, a rekord nie jest sealed
. Jest to błąd, jeśli metoda syntetyzowana lub jawnie zadeklarowana nie zastępuje object.ToString()
(na przykład z powodu cieniowania w typach baz pośrednich itp.).
Metoda syntetyzowana:
- tworzy wystąpienie
StringBuilder
, - Dołącza nazwę rekordu do konstruktora, a następnie " { ",
- wywołuje metodę
PrintMembers
rekordu, dając jej konstruktor, a następnie " , jeśli zwrócił wartość true, - dołącza "}",
- Zwraca zawartość konstruktora z
builder.ToString()
.
Rozważmy na przykład następujące typy rekordów:
record R1(T1 P1);
record R2(T1 P1, T2 P2, T3 P3) : R1(P1);
W przypadku tych typów rekordów syntetyzowane elementy drukowania będą wyglądać mniej więcej tak:
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();
}
}
Składowe rekordów pozycyjnych
Oprócz powyższych członków, rekordy z listą parametrów ("rekordy pozycyjne") tworzą dodatkowych członków z tymi samymi warunkami, co powyższe członki.
Konstruktor podstawowy
Typ rekordu ma publiczny konstruktor, którego podpis odpowiada parametrom wartości deklaracji typu. Jest to nazywane konstruktorem podstawowym dla typu i powoduje, że niejawnie zadeklarowany domyślny konstruktor klasy, jeśli istnieje, jest pomijany. Błędem jest posiadanie konstruktora głównego oraz konstruktora o tej samej sygnaturze już obecnego w klasie.
W czasie wykonywania konstruktor podstawowy
wykonuje inicjatory wystąpień wyświetlane w treści klasy
wywołuje konstruktor klasy bazowej z argumentami podanymi w klauzuli
record_base
, jeśli jest obecna
Jeśli rekord ma konstruktor podstawowy, każdy konstruktor zdefiniowany przez użytkownika, z wyjątkiem "konstruktora kopiowania" musi mieć jawny inicjator konstruktora this
.
Parametry konstruktora podstawowego, a także członkowie rekordu są dostępne w zakresie w argument_list
klauzuli record_base
oraz w inicjalizatorach pól lub właściwości instancji. Elementy członkowskie wystąpienia byłyby błędne w tych przypadkach (podobnie jak elementy członkowskie wystąpienia w zakresie w zwykłych inicjatorach konstruktorów są dzisiaj, ale ich użycie stanowi błąd), jednak parametry konstruktora głównego byłyby dostępne i możliwe do użycia, a także przesłaniałyby obecne elementy członkowskie. Statyczne elementy członkowskie byłyby również użyteczne, podobnie jak wywołania bazowe i inicjatory działają w zwykłych konstruktorach dzisiaj.
Ostrzeżenie jest generowane, jeśli parametr podstawowego konstruktora nie jest odczytywany.
Zmienne wyrażeń zadeklarowane w argument_list
są dostępne w zakresie argument_list
. Mają zastosowanie te same reguły cieniowania co na liście argumentów zwykłego inicjatora konstruktora.
Właściwości
Dla każdego parametru typu rekordu w deklaracji typu rekordu istnieje odpowiadająca mu publiczna właściwość, której nazwa i typ są pobierane z deklaracji parametru wartości.
Dla przypomnienia:
- Tworzona jest publiczna właściwość
get
i automatyczna właściwośćinit
(zobacz oddzielną specyfikację akcesorówinit
). Właściwośćabstract
dziedziczona z pasującym typem jest zastępowana. Jest to błąd, jeśli dziedziczona właściwość nie ma metod dostępupublic
,get
iinit
, które można przesłonić. Jest to błąd, jeśli dziedziczona właściwość jest ukryta.
Właściwość automatyczna jest inicjalizowana wartością odpowiadającego mu podstawowego parametru konstruktora. Atrybuty można stosować do zsyntetyzowanej właściwości automatycznej i jej pola pomocniczego, korzystając z obiektów docelowychproperty:
lubfield:
dla atrybutów składniowo przypisanych do odpowiedniego parametru rekordu.
Dekonstrukcja
Rekord pozycyjny z co najmniej jednym parametrem syntetyzuje publiczną metodę wystąpienia zwracającą pustkę o nazwie Deconstruct z deklaracją parametru out dla każdego parametru deklaracji konstruktora podstawowego. Każdy parametr metody Deconstruct
ma ten sam typ co odpowiadający mu parametr podstawowej deklaracji konstruktora. Treść metody przypisuje do każdego parametru metody Deconstruct
wartość właściwości wystąpienia o tej samej nazwie.
Metodę można zadeklarować jawnie. Jest to błąd, jeśli jawna deklaracja nie jest zgodna z oczekiwanym podpisem lub ułatwieniami dostępu lub jest statyczna.
W poniższym przykładzie przedstawiono rekord pozycyjny R
z metodą Deconstruct
syntetyzowaną przez kompilator, wraz z jej użyciem:
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}");
}
}
wyrażenie with
Wyrażenie with
to nowe wyrażenie używające następującej składni.
with_expression
: switch_expression
| switch_expression 'with' '{' member_initializer_list? '}'
;
member_initializer_list
: member_initializer (',' member_initializer)*
;
member_initializer
: identifier '=' expression
;
Wyrażenie with
nie jest dozwolone jako instrukcja.
Wyrażenie with
pozwala na "mutację niedestrukcyjną", przeznaczoną do tworzenia kopii wyrażenia odbiornika z modyfikacjami przypisań w member_initializer_list
.
Prawidłowe wyrażenie with
ma odbiornik o typie niepustym. Typ odbiornika musi być rekordem.
Po prawej stronie wyrażenia with
znajduje się member_initializer_list
z sekwencją przypisań w celu identyfikatora, które musi być polem wystąpienia lub właściwością typu odbiorcy.
Najpierw wywoływana jest metoda "clone" odbiorcy (określona powyżej), a jej wynik jest konwertowany na typ odbiorcy. Następnie każda member_initializer
jest przetwarzana w taki sam sposób, jak przypisanie do pola lub uzyskanie dostępu do właściwości wyniku konwersji. Zadania są przetwarzane według porządku leksykalnego.
C# feature specifications