Partager via


Structures d'enregistrement

Remarque

Cet article est une spécification de fonctionnalité. La spécification sert de document de conception pour la fonctionnalité. Il inclut les modifications de spécification proposées, ainsi que les informations nécessaires pendant la conception et le développement de la fonctionnalité. Ces articles sont publiés jusqu’à ce que les modifications de spécification proposées soient finalisées et incorporées dans la spécification ECMA actuelle.

Il peut y avoir des différences entre la spécification de la fonctionnalité et l’implémentation terminée. Ces différences sont consignées dans les notes pertinentes de la réunion de conception linguistique (LDM).

Vous pouvez en savoir plus sur le processus d’adoption des speclets de fonctionnalités dans la norme de langage C# dans l’article sur les spécifications .

Problème de champion : https://github.com/dotnet/csharplang/issues/4334

La syntaxe d’un struct d’enregistrement est la suivante :

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
    | ';'
    ;

Les types de structure d'enregistrement sont des types de valeur, comme d'autres types de structures. Ils héritent implicitement de la classe System.ValueType. Les modificateurs et les membres d'une structure d'enregistrement sont soumis aux mêmes restrictions que ceux des structures (accessibilité sur le type, modificateurs sur les membres, initialisateurs de constructeur d'instance base(...), affectation définitive pour this dans le constructeur, destructeurs,...). Les Record structs suivront également les mêmes règles que les structs pour les constructeurs d'instance sans paramètre et les initialisateurs de champ, mais ce document suppose que nous lèverons ces restrictions pour les structs en général.

Voir §16.4.9 Voir la spécification des constructeurs de structures sans paramètre.

Les structures d'enregistrement ne peuvent pas utiliser le modificateur ref.

Au maximum, une seule déclaration de type partiel d'une structure d'enregistrement partielle peut fournir un parameter_list. La parameter_list peut être vide.

Les paramètres de structure d’enregistrement ne peuvent pas utiliser les modificateurs ref, out ou this, mais in et params sont autorisés.

Membres d'une structure d'enregistrement

En plus des membres déclarés dans une structure d'enregistrement, un type de structure d'enregistrement dispose de membres générés supplémentaires. Les membres sont synthétisés, sauf si un membre avec une signature « correspondante » est déclaré dans le corps de la structure d’enregistrement ou si un membre concret non virtuel accessible avec une signature « correspondante » est hérité. 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. Voir Signatures et surcharge §7.6. Il est erroné qu'un membre d'une structure d'enregistrement soit nommé « Clone ».

Il s'agit d'une erreur qu'un champ d'instance d'une structure de données ait un type non sécurisé.

Une structure d'enregistrement n'est pas autorisée à déclarer un destructeur.

Les membres synthétisés sont les suivants :

Membres d’égalité

Les membres d'égalité synthétisés sont similaires à ceux d'une classe d'enregistrement (Equals pour ce type, Equals pour le type object, == et != opérateurs pour ce type),
à l'exception de l'absence de EqualityContract, de contrôles de nullité ou d'héritage.

La structure d'enregistrement implémente System.IEquatable<R> et inclut une surcharge synthétisée fortement typée de Equals(R other)R est la structure d'enregistrement. La méthode est public. 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.

Si Equals(R other) est défini par l’utilisateur (non synthétisé), mais GetHashCode n’est pas, un avertissement est généré.

public readonly bool Equals(R other);

Le Equals(R) synthétisé retourne true si et seulement si, pour chaque champ d’instance fieldN dans la structure d'enregistrement, la valeur de System.Collections.Generic.EqualityComparer<TN>.Default.Equals(fieldN, other.fieldN) dans laquelle TN est le type de champ est true.

La structure d’enregistrement comprend des opérateurs == et != synthétisés, équivalents aux opérateurs déclarés comme suit :

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

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.

La structure d'enregistrement inclut un remplacement généré automatiquement équivalent à une méthode déclarée comme suit :

public override readonly bool Equals(object? obj);

Il s'agit d'une erreur si le remplacement est déclaré explicitement. Le remplacement synthétisé renvoie other is R temp && Equals(temp)R est la structure record.

La structure d'enregistrement inclut un remplacement généré automatiquement équivalent à une méthode déclarée comme suit :

public override readonly int GetHashCode();

La méthode peut être déclarée explicitement.

Un avertissement est signalé si l’une des Equals(R) et GetHashCode() est explicitement déclarée, mais que l’autre méthode n’est pas explicite.

Le remplacement synthétisé de GetHashCode() renvoie un résultat int de la combinaison des valeurs de System.Collections.Generic.EqualityComparer<TN>.Default.GetHashCode(fieldN) pour chaque champ d'instance fieldN, TN étant le type de fieldN.

Par exemple, considérez le struct d’enregistrement suivant :

record struct R1(T1 P1, T2 P2);

Pour cette structure d'enregistrement, les membres d'égalité synthétisés seraient quelque chose comme :

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

Membres d'impression : PrintMembers et ToString

Le struct d’enregistrement inclut une méthode synthétisée équivalente à une méthode déclarée comme suit :

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

La méthode effectue les opérations suivantes :

  1. pour chacun des membres imprimables de la structure d'enregistrement (champs publics non statiques et membres de propriété lisibles), ajoute le nom de ce membre suivi de « = “ suivi de la valeur du membre séparée par ” , »,
  2. retourne vrai si la structure d'enregistrement a des membres imprimables.

Pour un membre qui a un type valeur, nous allons convertir sa valeur en représentation sous forme de chaîne à l’aide de la méthode la plus efficace disponible pour la plateforme cible. À l’heure actuelle, cela signifie appeler ToString avant de passer à StringBuilder.Append.

Si les membres imprimables de l'enregistrement n'incluent pas de propriété lisible avec un non-accesseurreadonlyget, alors le PrintMembers synthétisé est readonly. Il n'est pas nécessaire que les champs de l'enregistrement soient readonly pour que la méthode PrintMembers soit readonly.

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.

Le struct d’enregistrement inclut une méthode synthétisée équivalente à une méthode déclarée comme suit :

public override string ToString();

Si la méthode PrintMembers du struct d’enregistrement est readonly, la méthode de ToString() synthétisée est readonly.

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.

Méthode synthétisée :

  1. crée une instance StringBuilder,
  2. ajoute le nom de la structure d'enregistrement au constructeur, suivi de « { »,
  3. invoque la méthode PrintMembers de la structure d'enregistrement en lui donnant le constructeur, suivi de « » si le résultat est vrai,
  4. ajoute « } »,
  5. retourne le contenu du builder avec builder.ToString().

Par exemple, considérez le struct d’enregistrement suivant :

record struct R1(T1 P1, T2 P2);

Pour cette structure d'enregistrement, les membres d'impression synthétisés ressembleraient à quelque chose comme :

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();
    }
}

Membres de la structure d'enregistrement positionnels

En plus des membres ci-dessus, les structs d’enregistrement 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 principal

Un struct d’enregistrement a un constructeur public dont la signature correspond aux paramètres de valeur de la déclaration de type. Il s’agit du constructeur principal de ce type. C'est une erreur d'avoir un constructeur primaire et un constructeur avec la même signature déjà présents dans la structure. Si la déclaration de type n’inclut pas de liste de paramètres, aucun constructeur principal n’est généré.

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

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

Les déclarations de champs d'instance pour une structure de données sont autorisées à inclure des initialiseurs de variables. S’il n’existe aucun constructeur principal, les initialiseurs d’instance s’exécutent dans le cadre du constructeur sans paramètre. Sinon, au moment de l'exécution, le constructeur primaire exécute les initialisateurs d'instance apparaissant dans le corps de la structure d'enregistrement.

Si un struct d’enregistrement a un constructeur principal, tout constructeur défini par l’utilisateur doit avoir un initialiseur de constructeur this explicite qui appelle le constructeur principal ou un constructeur déclaré explicitement.

Les paramètres du constructeur principal ainsi que les membres de la structure de registre sont disponibles dans les initialiseurs des champs d’instance ou des propriétés. 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 peuvent également être utilisés.

Un avertissement est généré si un paramètre du constructeur principal n’est pas lu.

Les règles d'affectation définies pour les constructeurs d'instances de structures s'appliquent au constructeur primaire des structures d'enregistrement. Par exemple, voici une erreur :

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

Propriétés

Pour chaque paramètre d'une déclaration de structure 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 une structure d'enregistrement :

  • Une auto-propriété publique get et init est créée si la record struct a le modificateur readonly, get et set sinon. Les deux types d’accesseurs set (set et init) sont considérés comme « équivalents ». Par conséquent, l'utilisateur peut déclarer une propriété uniquement initialisable à la place d'une propriété mutable synthétisée. Une propriété abstract héritée avec un type correspondant est substituée. Aucune propriété automatique n’est créée si le struct d’enregistrement a un champ d’instance portant le nom et le type attendus. Il s’agit d’une erreur si la propriété héritée n’a pas publicget et set/init accesseurs. Il s’agit d’une erreur si la propriété ou le champ hérité est masqué.
    La propriété automatique est initialisée à la valeur du paramètre de constructeur principal correspondant. Les attributs peuvent être appliqués à la propriété auto-synthétisée et à son champ de stockage en utilisant les cibles property: ou field: pour les attributs appliqués syntaxiquement au paramètre correspondant de la structure d'enregistrement.

Déconstruire

Une structure d'enregistrement positionnelle avec au moins un paramètre synthétise une méthode d'instance publique à retour nul appelée Deconstruct avec une déclaration de paramètre out pour chaque paramètre de la déclaration du constructeur primaire. Chaque paramètre de la méthode Deconstruct a le même type que le paramètre correspondant de la déclaration du constructeur principal. Le corps de la méthode affecte chaque paramètre de la méthode Deconstruct à la valeur d'un membre d'instance accédant à un membre du même nom. Si les membres d'instance accédés dans le corps ne comprennent pas de propriété avec un non-accesseurreadonlyget, alors la méthode Deconstruct synthétisée est readonly. 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.

Autoriser l'expression with sur les structures

Il est désormais possible que le récepteur d'une expression with soit de type struct.

À droite de l'expression with se trouve un member_initializer_list avec une séquence d'affectations à l'identificateur , qui doit être un champ d'instance ou une propriété accessible du type du récepteur.

Pour un récepteur avec un type de struct, le récepteur est d’abord copié, puis chaque member_initializer est traité de la même façon qu’une affectation à un champ ou un accès aux propriétés du résultat de la conversion. Les affectations sont traitées dans l'ordre lexical.

Améliorations apportées aux enregistrements

Autoriser record class

La syntaxe existante pour les types d’enregistrements permet record class avec la même signification que record:

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

Autoriser les membres positionnels définis par l’utilisateur à être des champs

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

Aucune propriété automatique n’est créée si l’enregistrement a ou hérite d’un champ d’instance portant le nom et le type attendus.

Autoriser les constructeurs sans paramètre et les initialiseurs de membres dans les structures

Voir la spécification des constructeurs de structures sans paramètre.

Questions ouvertes

  • comment reconnaître les structs d’enregistrement dans les métadonnées ? (nous n'avons pas de méthode clone innommable à exploiter...)

Répondu

  • vérifiez que nous voulons conserver la conception PrintMembers (méthode distincte retournant bool) (réponse : oui)
  • confirmer que nous n’autoriserons pas record ref struct (problème avec les champs IEquatable<RefStruct> et ref) (réponse : oui)
  • confirmer l'implémentation des membres d'égalité. L'alternative est que bool Equals(R other) synthétisé, bool Equals(object? other) et les opérateurs soient tous simplement délégués à ValueType.Equals. (réponse : oui)
  • vérifiez que nous voulons autoriser les initialiseurs de champ lorsqu’il existe un constructeur principal. Voulons-nous également autoriser les constructeurs de structs sans paramètre pendant que nous y sommes (le problème de l’activateur a été apparemment résolu) ? (réponse : oui, les spécifications mises à jour doivent être examinées dans LDM)
  • Combien voulons-nous en dire sur la méthode Combine ? (réponse : le moins possible)
  • devrions-nous interdire un constructeur défini par l’utilisateur avec une signature de constructeur de copie ? (réponse : non, il n’existe aucune notion de constructeur de copie dans la spécification des structs d’enregistrement)
  • confirmez que nous voulons interdire les membres nommés « Clone ». (réponse : correct)
  • Vérifiez deux fois que la logique de Equals synthétisée est fonctionnellement équivalente à l’implémentation du runtime (par exemple, float. NaN) (réponse : confirmée dans LDM)
  • Les attributs de ciblage de champ ou de propriété peuvent-ils être placés dans la liste des paramètres positionnels ? (réponse : oui, comme pour la classe record)
  • with sur les génériques ? (réponse : hors de portée pour C# 10)
  • GetHashCode doit-il inclure un hachage du type lui-même, pour obtenir des valeurs différentes entre record struct S1; et record struct S2; ? (réponse : non)