Enregistrements
Remarque
Cet article est une spécification de fonctionnalité. La spécification sert de document de conception pour la fonctionnalité. Elle inclut les changements de spécification proposés, ainsi que les informations nécessaires à la conception et au développement de la fonctionnalité. Ces articles sont publiés jusqu'à ce que les changements proposés soient finalisés et incorporés dans la spécification ECMA actuelle.
Il peut y avoir des différences entre la spécification de la fonctionnalité et l'implémentation réalisée. Ces différences sont consignées dans les notes pertinentes de la réunion de conception linguistique (LDM).
Pour en savoir plus sur le processus d'adoption des speclets de fonctionnalité dans la norme du langage C#, consultez l'article sur les spécifications.
Problème de champion : https://github.com/dotnet/csharplang/issues/39
Cette proposition suit la spécification de la fonctionnalité des enregistrements C# 9, conformément à l'accord de l’équipe de conception du langage C#.
La syntaxe d'un enregistrement est la suivante :
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* '}' ';'?
| ';'
;
Les types d'enregistrement sont des types de référence, similaires à une déclaration de classe. Il s’agit d’une erreur lorsqu’un type d’enregistrement fournit un record_base
argument_list
si le record_declaration
ne contient pas de parameter_list
.
Au maximum, une seule déclaration de type partiel d’un type d’enregistrement partiel peut fournir un parameter_list
.
Les paramètres d'enregistrement ne peuvent pas utiliser les modificateurs ref
, out
ou this
(mais in
et params
sont autorisés).
Héritage
Les enregistrements ne peuvent pas hériter de classes, sauf si la classe est object
, et les classes ne peuvent pas hériter d'enregistrements. Les enregistrements peuvent hériter d’enregistrements existants.
Membres d’un type d’enregistrement
En plus des membres déclarés dans le corps de l'enregistrement, un type d'enregistrement possède des membres synthétisés supplémentaires. Les membres sont synthétisés, sauf si un membre avec une signature « correspondante » est déclaré dans le corps de l’enregistrement, ou si un membre non virtuel concret et accessible avec une signature « correspondante » est hérité. Un membre correspondant empêche le compilateur de générer ce membre, mais pas les autres membres synthétisés. Deux membres sont considérés comme correspondant s’ils ont la même signature ou seraient considérés comme « hiding » dans un scénario d’héritage. Il s’agit d’une erreur lorsqu’un membre d’un type d’enregistrement porte le nom « Clone ». C'est une erreur qu'un champ d'instance d'un enregistrement ait un type de pointeur de plus haut niveau. Un type de pointeur imbriqué, par exemple un tableau de pointeurs, est autorisé.
Les membres synthétisés sont les suivants :
Membres d'égalité
Si l'enregistrement est dérivé de object
, le type d'enregistrement comprend une propriété synthétisée en lecture seule équivalente à une propriété déclarée comme suit :
Type EqualityContract { get; }
La propriété est private
si le type d’enregistrement est sealed
. Sinon, la propriété est virtual
et protected
.
La propriété peut être déclarée explicitement. Il s'agit d'une erreur si la déclaration explicite ne correspond pas à la signature ou à l'accessibilité attendue, ou si la déclaration explicite n'autorise pas le remplacement dans un type dérivé et que le type d'enregistrement n'est pas sealed
.
Si le type d'enregistrement est dérivé d'un type d'enregistrement de base Base
, le type d'enregistrement comprend une propriété synthétisée en lecture seule équivalente à une propriété déclarée comme suit :
protected override Type EqualityContract { get; }
La propriété peut être déclarée explicitement. Il s'agit d'une erreur si la déclaration explicite ne correspond pas à la signature ou à l'accessibilité attendue, ou si la déclaration explicite n'autorise pas le remplacement dans un type dérivé et que le type d'enregistrement n'est pas sealed
. Il y a erreur si une propriété synthétisée ou déclarée explicitement ne remplace pas une propriété ayant cette signature dans le type d'enregistrement Base
(par exemple, si la propriété est manquante dans le Base
, ou scellée, ou non virtuelle, etc.)
La propriété synthétisée renvoie typeof(R)
où R
est le type d'enregistrement.
Le type d’enregistrement implémente System.IEquatable<R>
et inclut une surcharge fortement typée synthétisée de Equals(R? other)
où R
est le type d’enregistrement.
La méthode est public
, et la méthode est virtual
sauf si le type d'enregistrement est sealed
.
La méthode peut être déclarée explicitement. C'est une erreur si la déclaration explicite ne correspond pas à la signature ou à l'accessibilité attendue, ou si la déclaration explicite ne permet pas de la remplacer dans un type dérivé et que le type d'enregistrement n'est pas sealed
.
Si Equals(R? other)
est défini par l'utilisateur (non synthétisé) mais que GetHashCode
ne l'est pas, un avertissement est produit.
public virtual bool Equals(R? other);
Le Equals(R?)
synthétisé renvoie true
si et seulement si chacun des éléments suivants est true
:
-
other
n'est pasnull
, et - Pour chaque champ d’instance
fieldN
dans le type d’enregistrement qui n’est pas hérité, la valeur deSystem.Collections.Generic.EqualityComparer<TN>.Default.Equals(fieldN, other.fieldN)
oùTN
est le type du champ, et - S'il existe un type d'enregistrement de base, la valeur de
base.Equals(other)
(un appel non virtuel àpublic virtual bool Equals(Base? other)
) ; sinon, la valeur deEqualityContract == other.EqualityContract
.
Le type d’enregistrement comprend des opérateurs ==
synthétisés et des opérateurs !=
équivalents aux opérateurs déclarés comme suit :
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);
La méthode Equals
appelée par l'opérateur ==
est la méthode Equals(R? other)
spécifiée ci-dessus. L'opérateur !=
délègue à l'opérateur ==
. Il s’agit d’une erreur si les opérateurs sont déclarés explicitement.
Si le type d'enregistrement est dérivé d'un type d'enregistrement de base Base
, le type d'enregistrement inclut un remplacement synthétisé équivalent à une méthode déclarée comme suit :
public sealed override bool Equals(Base? other);
Il s’agit d’une erreur si le remplacement est déclaré explicitement. Il s'agit d'une erreur si la méthode ne remplace pas une méthode de même signature dans le type d'enregistrement Base
(par exemple, si la méthode est absente dans le Base
, ou scellée, ou non virtuelle, etc.)
La substitution synthétisée renvoie Equals((object?)other)
.
Le type d'enregistrement comprend un remplacement synthétisé équivalent à une méthode déclarée comme suit :
public override bool Equals(object? obj);
Il s’agit d’une erreur si le remplacement est déclaré explicitement. Il s'agit d'une erreur si la méthode ne remplace pas object.Equals(object? obj)
(par exemple, en raison d'une ombre dans les types de base intermédiaires, etc.)
Le remplacement synthétisé renvoie Equals(other as R)
où R
est le type d'enregistrement.
Le type d'enregistrement comprend un remplacement synthétisé équivalent à une méthode déclarée comme suit :
public override int GetHashCode();
La méthode peut être déclarée explicitement.
Il s’agit d’une erreur lorsque la déclaration explicite n’autorise pas la substitution dans un type dérivé et que le type d’enregistrement n’est pas sealed
. Une erreur est signalée si la méthode synthétisée ou explicitement déclarée ne remplace pas object.GetHashCode()
(par exemple, en raison d'une ombre dans les types de base intermédiaires, etc.)
Un avertissement est signalé si l'une des méthodes Equals(R?)
et GetHashCode()
est explicitement déclarée, mais que l'autre n'est pas explicite.
Le remplacement synthétisé de GetHashCode()
renvoie un résultat int
de la combinaison des valeurs suivantes :
- Pour chaque champ d’instance
fieldN
dans le type d’enregistrement qui n’est pas hérité, la valeur deSystem.Collections.Generic.EqualityComparer<TN>.Default.GetHashCode(fieldN)
oùTN
est le type du champ, et - S'il existe un type d'enregistrement de base, la valeur de
base.GetHashCode()
; sinon la valeur deSystem.Collections.Generic.EqualityComparer<System.Type>.Default.GetHashCode(EqualityContract)
.
Par exemple, considérons les types d'enregistrement suivants :
record R1(T1 P1);
record R2(T1 P1, T2 P2) : R1(P1);
record R3(T1 P1, T2 P2, T3 P3) : R2(P1, P2);
Pour ces types d’enregistrement, les membres d’égalité synthétisés ressembleraient à ceci :
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));
}
}
Copier et cloner des membres
Un type d’enregistrement contient deux membres de copie :
- Un constructeur prenant un seul argument du type d’enregistrement. On l’appelle un « constructeur de copie ».
- Une méthode publique synthétisée d'instance sans paramètre « clone » avec un nom réservé par le compilateur
Le but du constructeur copy est de copier l'état du paramètre vers la nouvelle instance créée. Ce constructeur n'exécute aucun initialisateur de champ/propriété d'instance présent dans la déclaration de l'enregistrement. Si le constructeur n'est pas explicitement déclaré, un constructeur sera synthétisé par le compilateur. Si l’enregistrement est « sealed », le constructeur sera « private », sinon il sera « protected ». Un constructeur de copie explicitement déclaré doit être public ou protégé, à moins que l'enregistrement ne soit scellé. La première chose que le constructeur doit faire est d'appeler un constructeur de copie de la base, ou un constructeur d'objet sans paramètre si l'enregistrement hérite d'un objet. Une erreur est signalée si un constructeur de copie défini par l'utilisateur utilise un initialisateur de constructeur implicite ou explicite qui ne remplit pas cette condition. Après l'invocation d'un constructeur de copie de base, un constructeur de copie synthétisé copie les valeurs de tous les champs d'instance implicitement ou explicitement déclarés dans le type d'enregistrement. La seule présence d'un constructeur de copie, qu'il soit explicite ou implicite, n'empêche pas l'ajout automatique d'un constructeur d'instance par défaut.
Si une méthode virtuelle « clone » est présente dans l'enregistrement de base, la méthode « clone » synthétisée la remplace et le type de retour de la méthode est le type contenant actuel. Une erreur est générée si la méthode de clonage de l'enregistrement de base est verrouillée. Si une méthode « clone » virtuelle n'est pas présente dans l'enregistrement de base, le type de retour de la méthode clone est le type contenant et la méthode est virtuelle, sauf si l'enregistrement est scellé ou abstrait. Si l’enregistrement englobant est « abstract », la méthode clone synthétisée est également « abstract ». Si la méthode « clone » n'est pas abstraite, elle renvoie le résultat d'un appel à un constructeur de copie.
Membres d’impression : PrintMembers et ToString
Si l'enregistrement est dérivé de object
, il comprend une méthode synthétisée équivalente à une méthode déclarée comme suit :
bool PrintMembers(System.Text.StringBuilder builder);
La méthode est private
si le type d'enregistrement est sealed
. Sinon, la méthode est virtual
et protected
.
La méthode :
- appelle la méthode
System.Runtime.CompilerServices.RuntimeHelpers.EnsureSufficientExecutionStack()
si la méthode est présente et que l’enregistrement possède des membres imprimables. - pour chacun des membres imprimables de l’enregistrement (champ public non statique et membres de propriété lisible), ajoute le nom de ce membre suivi de " = " suivi de la valeur de ce membre, séparés par ", ",
- renvoie true si l’enregistrement possède des membres imprimables.
Pour un membre qui a un type de valeur, nous convertirons sa valeur en une représentation sous forme de chaîne de caractères en utilisant la méthode la plus efficace disponible sur la plateforme cible. Actuellement, cela signifie appeler ToString
avant de passer à StringBuilder.Append
.
Si le type d'enregistrement est dérivé d'un enregistrement de base Base
, l'enregistrement comprend un remplacement synthétisé équivalent à une méthode déclarée comme suit :
protected override bool PrintMembers(StringBuilder builder);
Si l'enregistrement n'a pas de membres imprimables, la méthode appelle la méthode de base PrintMembers
avec un argument (son paramètre builder
) et renvoie le résultat.
Sinon, la méthode :
- appelle la méthode de base
PrintMembers
avec un argument (son paramètrebuilder
), - si la méthode
PrintMembers
a renvoyé true, ajoute ", " au builder, - pour chaque membre imprimable de l'enregistrement, ajouter le nom de ce membre suivi de « = » suivi de la valeur du membre :
this.member
(outhis.member.ToString()
pour les types de valeur), séparés par « , », - retourne true.
La méthode PrintMembers
peut être déclarée explicitement.
Il s'agit d'une erreur si la déclaration explicite ne correspond pas à la signature ou à l'accessibilité attendue, ou si la déclaration explicite n'autorise pas le remplacement dans un type dérivé et que le type d'enregistrement n'est pas sealed
.
L'enregistrement comprend une méthode synthétisée équivalente à une méthode déclarée comme suit :
public override string ToString();
La méthode peut être déclarée explicitement. Il s'agit d'une erreur si la déclaration explicite ne correspond pas à la signature ou à l'accessibilité attendue, ou si la déclaration explicite n'autorise pas le remplacement dans un type dérivé et que le type d'enregistrement n'est pas sealed
. C'est une erreur si l'une ou l'autre méthode synthétisée, ou déclarée explicitement, ne remplace pas object.ToString()
(par exemple, en raison d'un shadowing dans les types de base intermédiaires, etc.)
La méthode synthétisée :
- crée une instance de
StringBuilder
, - ajoute le nom de l’enregistrement au builder, suivi de " { ",
- appelle la méthode
PrintMembers
de l’enregistrement en lui passant le builder, suivi de " " s’il a renvoyé true, - ajoute "}",
- retourne le contenu du builder avec
builder.ToString()
.
Par exemple, considérons les types d'enregistrement suivants :
record R1(T1 P1);
record R2(T1 P1, T2 P2, T3 P3) : R1(P1);
Pour ces types d’enregistrements, les éléments d’impression synthétisés pourraient ressembler à :
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();
}
}
Membres positionnels d’un type d’enregistrement
En plus des membres ci-dessus, les enregistrements avec une liste de paramètres (« enregistrements positionnels ») synthétisent des membres supplémentaires avec les mêmes conditions que les membres ci-dessus.
Constructeur primaire
Un type d'enregistrement possède un constructeur public dont la signature correspond aux paramètres de valeur de la déclaration du type. C'est ce qu'on appelle le constructeur primaire du type, et il entraîne la suppression du constructeur de classe par défaut implicitement déclaré, s'il est présent. Avoir un constructeur primaire et un constructeur avec la même signature déjà présents dans la classe constitue une erreur.
Au moment de l’exécution, le constructeur primaire
exécute les initialisateurs d'instance apparaissant dans le corps de la classe
invoque le constructeur de la classe de base avec les arguments fournis dans la clause
record_base
, le cas échéant.
Si un enregistrement possède un constructeur primaire, tout constructeur défini par l'utilisateur, à l'exception du « constructeur de copie », doit avoir un initialisateur de constructeur this
explicite.
Les paramètres du constructeur primaire ainsi que les membres de l’enregistrement sont visibles dans le argument_list
de la clause record_base
et dans les initialiseurs des champs ou propriétés d’instance. Les membres d’instance situé à ces emplacements constitueraient une erreur (de la même façon dont les membres d’instance sont visibles dans les initialiseurs de constructeur habituels, mais qu’il est erroné de les utiliser), mais les paramètres du constructeur primaire seraient visibles et utilisables et masqueraient les membres. Les membres statiques seraient également utilisables, de la même manière que les appels de base et les initialisateurs fonctionnent aujourd'hui dans les constructeurs ordinaires.
Un avertissement est émis si un paramètre du constructeur primaire n'est pas lu.
Les variables d’expression déclarées dans la argument_list
sont accessibles au sein de la argument_list
. Les mêmes règles d'ombrage que dans une liste d'arguments d'un initialisateur de constructeur ordinaire s'appliquent.
Propriétés
Pour chaque paramètre d'enregistrement d'une déclaration de type d'enregistrement, il existe un membre de propriété publique correspondant dont le nom et le type sont tirés de la déclaration du paramètre de valeur.
Pour mémoire :
- Une auto-propriété publique
get
etinit
est créée (voir spécification d’accesseurinit
distincte). Une propriétéabstract
héritée avec un type correspondant est substituée. Cela constitue une erreur si la propriété héritée ne dispose pas d’accesseurspublic
etget
init
substituables. C'est une erreur si la propriété héritée est cachée.
L'auto-propriété est initialisée à la valeur du paramètre du constructeur primaire correspondant. Les attributs peuvent être appliqués à la propriété automatique synthétisée et à son champ de stockage en utilisant les ciblesproperty:
oufield:
pour les attributs appliqués syntaxiquement au paramètre d’enregistrement correspondant.
Déconstruire
Un enregistrement positionnel avec au moins un paramètre synthétise une méthode d'instance publique renvoyant void appelée Deconstruct avec une déclaration de paramètre out pour chaque paramètre de la déclaration du constructeur principal. Chaque paramètre de la méthode Deconstruct
a le même type que le paramètre correspondant de la déclaration du constructeur primaire. Le corps de la méthode affecte à chaque paramètre de la méthode Deconstruct
, la valeur de la propriété d'instance de même nom.
La méthode peut être déclarée explicitement. Il s'agit d'une erreur si la déclaration explicite ne correspond pas à la signature ou à l'accessibilité attendue, ou si elle est statique.
L’exemple suivant montre un enregistrement positionnel R
avec sa méthode Deconstruct
synthétisée par le compilateur, ainsi que son utilisation :
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
expression
Une expression with
est une nouvelle expression utilisant la syntaxe suivante.
with_expression
: switch_expression
| switch_expression 'with' '{' member_initializer_list? '}'
;
member_initializer_list
: member_initializer (',' member_initializer)*
;
member_initializer
: identifier '=' expression
;
Une expression with
n’est pas autorisée en tant qu’instruction.
Une expression with
permet une « mutation non destructrice », conçue pour produire une copie de l’expression réceptrice avec des modifications dans les attributions de member_initializer_list
.
Une expression with
valide a un récepteur dont le type n’est pas void. Le type du récepteur doit être un type d’enregistrement.
Sur le côté droit de l’expression with
se trouve un member_initializer_list
avec une séquence d’affectations vers identifier, qui doit être un champ ou une propriété d’instance accessible du type du récepteur.
Tout d'abord, la méthode « clone » du récepteur (spécifiée ci-dessus) est invoquée et son résultat est converti dans le type du récepteur. Ensuite, chaque member_initializer
est traité de la même manière qu'une affectation à un champ ou un accès à une propriété du résultat de la conversion. Les affectations sont traitées dans l’ordre lexical.
C# feature specifications