Partage via


Membres requis

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/3630

Récapitulatif

Cette proposition ajoute un moyen de spécifier qu’une propriété ou un champ doit être défini(e) lors de l’initialisation d’un objet, forçant ainsi le créateur d’instance à fournir une valeur initiale pour le membre dans un initialiseur d’objet sur le site de création.

Motivation

Les hiérarchies d’objets nécessitent aujourd’hui beaucoup de code répétitif pour faire passer des données à tous les niveaux de la hiérarchie. Examinons une hiérarchie simple impliquant une classe Person telle qu’elle peut être définie dans C# 8 :

class Person
{
    public string FirstName { get; }
    public string MiddleName { get; }
    public string LastName { get; }

    public Person(string firstName, string lastName, string? middleName = null)
    {
        FirstName = firstName;
        LastName = lastName;
        MiddleName = middleName ?? string.Empty;
    }
}

class Student : Person
{
    public int ID { get; }
    public Student(int id, string firstName, string lastName, string? middleName = null)
        : base(firstName, lastName, middleName)
    {
        ID = id;
    }
}

Il y a beaucoup de répétitions ici :

  1. À la racine de la hiérarchie, le type de chaque propriété devait être répété deux fois, et le nom devait être répété quatre fois.
  2. Au niveau dérivé, le type de chaque propriété héritée devait être répété une fois, et le nom devait être répété deux fois.

Il s’agit d’une hiérarchie simple avec 3 propriétés et 1 niveau d’héritage, mais de nombreux exemples réels de ces types de hiérarchies vont à des niveaux beaucoup plus profonds, accumulant un nombre de plus en plus grand de propriétés à transmettre. Roslyn est un exemple de base de code, notamment dans les différents types d’arbres qui composent nos CST et AST. Cet emboîtement est suffisamment fastidieux pour que nous ayons recours à des générateurs de code pour créer les constructeurs et définitions de ces types, et de nombreux clients adoptent des approches similaires face à ce problème. C# 9 introduit des enregistrements, qui, pour certains scénarios, peuvent améliorer ce qui suit :

record Person(string FirstName, string LastName, string MiddleName = "");
record Student(int ID, string FirstName, string LastName, string MiddleName = "") : Person(FirstName, LastName, MiddleName);

Les records éliminent la première source de duplication, mais la deuxième source de duplication reste inchangée : malheureusement, c’est cette source de duplication qui croît avec la hiérarchie et constitue la partie la plus pénible à corriger après une modification de la hiérarchie, car elle nécessite de suivre la hiérarchie dans tous ses emplacements, éventuellement à travers plusieurs projets, ce qui risque de perturber les consommateurs.

Pour éviter la duplication de code, nous avons longtemps vu les utilisateurs adopter des initialiseurs d’objets comme un moyen d’éviter d’écrire des constructeurs. Avant C# 9, cependant, cela présentait deux inconvénients majeurs :

  1. La hiérarchie d’objets doit être entièrement mutable, avec des set accesseurs sur chaque propriété.
  2. Il n’existe aucun moyen de s’assurer que chaque instanciation d’un objet à partir du graphe définit chaque membre.

C# 9 a de nouveau résolu le premier problème ici, en introduisant l’accesseur init : avec lui, ces propriétés peuvent être définies lors de la création/initialisation de l’objet, mais pas par la suite. Toutefois, nous avons encore le deuxième problème : les propriétés en C# sont facultatives depuis C# 1.0. Les types de référence nullables, introduits dans C# 8.0, ont résolu une partie de ce problème : si un constructeur n’initialise pas une propriété de type référence non nullable, alors l’utilisateur en est averti. Cependant, cela ne résout pas le problème : l'utilisateur souhaite ne pas répéter de grandes parties de son type dans le constructeur, il souhaite transmettre l'exigence de définir des propriétés à ses consommateurs. Il ne fournit pas non plus d’avertissements à propos de ID de Student, car il s’agit d’un type de valeur. Ces scénarios sont extrêmement courants dans les modèle de base de données ORM, tels qu’EF Core, qui doivent avoir un constructeur public sans paramètre, mais qui définissent ensuite la nullabilité des lignes en fonction de la nullabilité des propriétés.

Cette proposition vise à répondre à ces préoccupations en introduisant une nouvelle fonctionnalité dans C# : les membres requis. Les membres requis devront être initialisés par les consommateurs, plutôt que par l’auteur du type, avec différentes personnalisations pour permettre la flexibilité pour plusieurs constructeurs et d’autres scénarios.

Conception détaillée

Les types class, struct et record gagnent la capacité de déclarer une liste_des_membres_requis. Cette liste est celle de toutes les propriétés et champs d’un type considérés comme requis et doit être initialisée pendant la construction et l’initialisation d’une instance du type. Les types héritent automatiquement de ces listes à partir de leurs types de base, ce qui offre une expérience transparente qui supprime le code réutilisable et répétitif.

Modificateur required

Nous ajoutons 'required' à la liste des modificateurs dans field_modifier et property_modifier. La required_member_list (liste_des_membres_requis) d’un type est composée de tous les membres auxquels required a été appliqué. Par conséquent, le type Person de la version antérieure ressemble maintenant à ceci :

public class Person
{
    // The default constructor requires that FirstName and LastName be set at construction time
    public required string FirstName { get; init; }
    public string MiddleName { get; init; } = "";
    public required string LastName { get; init; }
}

Tous les constructeurs d’un type possédant une required_member_list annoncent automatiquement un contrat qui exige que les consommateurs du type initialisent toutes les propriétés de la liste. C'est une erreur qu'un constructeur rende public un contrat qui exige un membre qui n'est pas au moins aussi accessible que le constructeur lui-même. Exemple :

public class C
{
    public required int Prop { get; protected init; }

    // Advertises that Prop is required. This is fine, because the constructor is just as accessible as the property initer.
    protected C() {}

    // Error: ctor C(object) is more accessible than required property Prop.init.
    public C(object otherArg) {}
}

required n’est valide que dans les types class, struct et record. Il n’est pas valide dans les types interface. required ne peut pas être combiné avec les modificateurs suivants :

  • fixed
  • ref readonly
  • ref
  • const
  • static

required n’est pas autorisé à être appliqué aux indexeurs.

Le compilateur émettra un avertissement lorsque Obsolete est appliqué à un membre requis d’un type et :

  1. Le type n’est pas marqué Obsolete, ou
  2. Tout constructeur non attribué avec SetsRequiredMembersAttribute n’est pas marqué comme Obsolete.

SetsRequiredMembersAttribute

Tous les constructeurs d’un type avec des membres requis, ou dont le type de base spécifie les membres requis, doivent avoir ces membres définis par un consommateur lorsque ce constructeur est appelé. Pour exempter les constructeurs de cette exigence, un constructeur peut être attribué avec SetsRequiredMembersAttribute, ce qui supprime ces exigences. Le corps du constructeur n’est pas validé pour s’assurer qu’il initialise bien les membres requis du type.

SetsRequiredMembersAttribute supprime toutes les exigences d’un constructeur, et ces exigences ne sont pas vérifiées quant à leur validité de quelque manière que ce soit. NB : il s’agit de la solution de contournement si l’héritage d’un type avec une liste de membres requis invalide est nécessaire : marquez le constructeur de ce type avec SetsRequiredMembersAttribute, et aucune erreur ne sera signalée.

Si un constructeur C se chaîne à un constructeur base ou this qui est attribué avec SetsRequiredMembersAttribute, alors C doit également être attribué avec SetsRequiredMembersAttribute.

Pour les types d’enregistrement, nous émettrons SetsRequiredMembersAttribute sur le constructeur de copie synthétisé d’un enregistrement si le type d’enregistrement ou l’un de ses types de base a des membres requis.

NB : Une version antérieure de cette proposition comportait un métalangage plus étendu autour de l’initialisation, ce qui permettait d’ajouter et de supprimer des membres requis individuels d’un constructeur, ainsi que la validation que le constructeur définissait correctement tous les membres requis. Cela a été jugé trop complexe pour la version initiale et supprimé. Nous pourrons envisager l’ajout de contrats et de modifications plus complexes ultérieurement.

Application de la loi

Pour chaque constructeur Ci dans un type T avec des membres requis R, les consommateurs appelant Ci doivent faire l’une des actions suivantes :

  • Initialiser tous les membres de R dans un object_initializer (initialiseur_d’objet) sur l’expression object_creation_expression (expression_création_d’objet),
  • Ou initialiser tous les membres de R via la section named_argument_list (liste_d’arguments_nommés) d’un attribute_target(cible_attribut).

sauf si Ci est attribué à SetsRequiredMembers.

Si le contexte actuel n’autorise pas un object_initializer ou n’est pas un attribute_target, et que Ci n’est pas attribué avec SetsRequiredMembers, alors c'est une erreur d'appeler Ci.

Contrainte new()

Un type avec un constructeur sans paramètre qui annonce un contrat n’est pas autorisé à être substitué pour un paramètre de type contraint à new(), car il n’existe aucun moyen pour l’instanciation générique de s’assurer que les exigences sont satisfaites.

struct defaults

Les membres requis ne sont pas appliqués aux instances des types struct créées avec default ou default(StructType). Elles sont appliquées pour les instances de struct créées avec new StructType(), même lorsque StructType n’a pas de constructeur sans paramètre et que le constructeur de struct par défaut est utilisé.

Accessibilité

Il est erroné de marquer un membre comme requis si ce membre ne peut pas être défini dans un contexte où le type contenant est visible.

  • Si le membre est un champ, il ne peut pas être readonly.
  • Si le membre est une propriété, il doit avoir un setter ou un initer au moins aussi accessible que le type contenant.

Cela signifie que les cas suivants ne sont pas autorisés :

interface I
{
    int Prop1 { get; }
}
public class Base
{
    public virtual int Prop2 { get; set; }

    protected required int _field; // Error: _field is not at least as visible as Base. Open question below about the protected constructor scenario

    public required readonly int _field2; // Error: required fields cannot be readonly
    protected Base() { }

    protected class Inner
    {
        protected required int PropInner { get; set; } // Error: PropInner cannot be set inside Base or Derived
    }
}
public class Derived : Base, I
{
    required int I.Prop1 { get; } // Error: explicit interface implementions cannot be required as they cannot be set in an object initializer

    public required override int Prop2 { get; set; } // Error: this property is hidden by Derived.Prop2 and cannot be set in an object initializer
    public new int Prop2 { get; }

    public required int Prop3 { get; } // Error: Required member must have a setter or initer

    public required int Prop4 { get; internal set; } // Error: Required member setter must be at least as visible as the constructor of Derived
}

C‘est une erreur de cacher un membre required, car ce membre ne peut plus être défini par un consommateur.

Lors de la surcharge d’un membre required, le mot-clé required doit être inclus dans la signature de la méthode. Cela permet, si nécessaire à l’avenir, de désactiver l’exigence pour une propriété dans une surcharge.

Les surcharges peuvent marquer un membre required alors qu’il n’était pas required dans le type de base. Un membre ainsi marqué est ajouté à la liste des membres requis du type dérivé.

Les types sont autorisés à remplacer les propriétés virtuelles requises. Cela signifie que si la propriété virtuelle de base dispose de stockage et que le type dérivé tente d’accéder à l’implémentation de base de cette propriété, un stockage non initialisé pourrait être observé. Remarque : il s’agit d’un anti-modèle C# général et nous pensons que cette proposition ne devrait pas tenter de le traiter.

Effet sur l’analyse de la nullité

Les membres marqués required ne sont pas tenus d’être initialisés à un état nullable valide à la fin d’un constructeur. Tous les membres required de ce type et des types de base sont considérés comme ayant une valeur par défaut au début de tout constructeur, sauf s’il est lié par chaînage à un constructeur this ou base annoté avec SetsRequiredMembersAttribute.

L’analyse nullable avertira pour tous les membres required actuels et des types de base qui n’ont pas un état nullable valide à la fin d’un constructeur annoté avec SetsRequiredMembersAttribute.

#nullable enable
public class Base
{
    public required string Prop1 { get; set; }

    public Base() {}

    [SetsRequiredMembers]
    public Base(int unused) { Prop1 = ""; }
}
public class Derived : Base
{
    public required string Prop2 { get; set; }

    [SetsRequiredMembers]
    public Derived() : base()
    {
    } // Warning: Prop1 and Prop2 are possibly null.

    [SetsRequiredMembers]
    public Derived(int unused) : base()
    {
        Prop1.ToString(); // Warning: possibly null dereference
        Prop2.ToString(); // Warning: possibly null dereference
    }

    [SetsRequiredMembers]
    public Derived(int unused, int unused2) : this()
    {
        Prop1.ToString(); // Ok
        Prop2.ToString(); // Ok
    }

    [SetsRequiredMembers]
    public Derived(int unused1, int unused2, int unused3) : base(unused1)
    {
        Prop1.ToString(); // Ok
        Prop2.ToString(); // Warning: possibly null dereference
    }
}

Représentation des métadonnées

Les deux attributs suivants sont connus du compilateur C# et requis pour que cette fonctionnalité fonctionne :

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
    public sealed class RequiredMemberAttribute : Attribute
    {
        public RequiredMemberAttribute() {}
    }
}

namespace System.Diagnostics.CodeAnalysis
{
    [AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)]
    public sealed class SetsRequiredMembersAttribute : Attribute
    {
        public SetsRequiredMembersAttribute() {}
    }
}

Il est erroné d'appliquer manuellement RequiredMemberAttribute à un type.

Un membre marqué required possède une RequiredMemberAttribute appliquée. En outre, tout type qui définit de tels membres est marqué avec RequiredMemberAttribute, en tant que marqueur pour indiquer qu’il existe des membres requis dans ce type. Notez que si le type B dérive de A et que A définit des membres required membres mais que B n’ajoute pas de nouveaux membres requis ou ne remplace pas des membres required existants, alors B ne sera pas marqué avec un RequiredMemberAttribute. Pour déterminer entièrement s’il existe des membres requis dans B, la vérification de la hiérarchie d’héritage complète est nécessaire.

Tout constructeur dans un type avec des membres required qui ne possède pas SetsRequiredMembersAttribute est marqué par deux attributs :

  1. System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute avec le nom de la fonctionnalité "RequiredMembers".
  2. System.ObsoleteAttribute avec la chaîne "Types with required members are not supported in this version of your compiler", et l’attribut est marqué comme une erreur pour empêcher les compilateurs plus anciens d’utiliser ces constructeurs.

Nous n’utilisons pas ici un modreq, car notre objectif est de maintenir la compatibilité binaire : si la dernière propriété required était supprimée d’un type, le compilateur ne synthétiserait plus ce modreq, ce qui constituerait un changement cassant de binaire et tous les consommateurs devraient être recompilés. Un compilateur qui comprend les membres required ignorera cet attribut obsolète. Notez que les membres peuvent également provenir de types de base : même s’il n’y a pas de nouveaux membres required dans le type actuel, si un type de base a des membres required, cet attribut Obsolete sera généré. Si le constructeur a déjà un attribut Obsolete, aucun attribut Obsolete supplémentaire ne sera généré.

Nous utilisons à la fois ObsoleteAttribute et CompilerFeatureRequiredAttribute, car ce dernier est nouveau dans cette version et les anciens compilateurs ne le comprennent pas. À l’avenir, nous pourrons peut-être abandonner ObsoleteAttribute et/ou ne pas l’utiliser pour protéger les nouvelles fonctionnalités, mais pour l’instant, nous avons besoin des deux pour une protection complète.

Pour générer la liste complète des membres required R pour un type donné T, en incluant tous les types de base, l’algorithme suivant est exécuté :

  1. Pour chaque Tb, à partir de T et en remontant la chaîne de types de base jusqu’à object.
  2. Si Tb est marqué avec RequiredMemberAttribute, alors tous les membres de Tb marqués avec RequiredMemberAttribute sont rassemblés dans Rb.
    1. Pour chaque Ri dans Rb, si Ri est remplacée par un membre de R, elle est ignorée.
    2. Sinon, si un Ri est caché par un membre de R, alors la recherche des membres requis échoue et aucune autre étape n’est effectuée. Appeler un constructeur de T sans l’attribution SetsRequiredMembers génère une erreur.
    3. Sinon, Ri est ajouté à R.

Questions en suspens

Initialiseurs de membres imbriqués

Quels sont les mécanismes d’application pour les initialiseurs de membres imbriqués ? Est-ce qu’ils seront complètement interdits ?

class Range
{
    public required Location Start { get; init; }
    public required Location End { get; init; }
}

class Location
{
    public required int Column { get; init; }
    public required int Line { get; init; }
}

_ = new Range { Start = { Column = 0, Line = 0 }, End = { Column = 1, Line = 0 } } // Would this be allowed if Location is a struct type?
_ = new Range { Start = new Location { Column = 0, Line = 0 }, End = new Location { Column = 1, Line = 0 } } // Or would this form be necessary instead?

Questions abordées

Niveau d’application des clauses init

La fonctionnalité de clause init n’a pas été implémentée dans C# 11. Elle reste une proposition active.

Appliquons-nous strictement la règle selon laquelle les membres spécifiés dans une clause init sans initialiseur doivent initialiser tous les membres ? Il semble probable que nous le fassions, sinon nous créons un gouffre d'échec facile. Toutefois, nous courons également le risque de réintroduire les mêmes problèmes que ceux que nous avons résolus avec MemberNotNull dans C# 9. Pour appliquer strictement cette règle, une méthode d’assistance doit pouvoir indiquer qu’elle définit un membre. Voici quelques syntaxes possibles que nous avons abordées :

  • Permettre les méthodes init. Ces méthodes ne peuvent être appelées qu’à partir d’un constructeur ou d’une autre méthode init, et peuvent accéder à this comme si elles étaient dans le constructeur (c’est-à-dire, définir les champs/propriétés readonly et init). Cela peut être combiné avec des clauses init pour ces méthodes. Une clause init serait considérée comme satisfaite si le membre en question est assigné de manière certaine dans le corps de la méthode ou du constructeur. Appeler une méthode avec une clause init qui inclut un membre équivaut à affecter ce membre. Si nous décidons de suivre cette voie, maintenant ou dans le futur, il semble probable que nous ne devrions pas utiliser init comme mot clé pour la clause init dans un constructeur, car cela serait source de confusion.
  • Autorisez l’opérateur ! à supprimer explicitement l’avertissement/erreur. Si vous initialisez un membre de manière compliquée (par exemple dans une méthode partagée), l’utilisateur peut ajouter un ! à la clause init pour indiquer que le compilateur ne doit pas vérifier l’initialisation.

Conclusion : Après discussion, l’idée de l’opérateur ! est appréciée. Cela permet à l’utilisateur d’être intentionnel dans des scénarios plus compliqués tout en ne créant pas de grand trou de conception autour des méthodes init et en annotant chaque méthode comme définissant les membres X ou Y. ! a été choisi étant donné que nous l’utilisons déjà pour supprimer des avertissements nullables, et l’utiliser pour indiquer au compilateur « Je suis plus intelligent que toi » dans un autre contexte est une extension naturelle de la forme syntaxique.

Membres d’interface requis

Cette proposition n’autorise pas les interfaces à marquer des membres comme requis. Cela nous protège des scénarios complexes autour de new() et des contraintes d’interface dans les génériques pour le moment, et cela concerne directement les fabriques et la construction générique. Pour nous assurer que nous disposons d’un espace de conception dans ce domaine, nous interdisons required dans les interfaces et interdisons que des types avec required_member_lists soient substitués par des paramètres de types contraints pour new(). Lorsque nous voulons examiner plus largement les scénarios de construction génériques avec des usines, nous pouvons réexaminer la question.

Questions de syntaxe

La fonctionnalité de clause init n’a pas été implémentée dans C# 11. Elle reste une proposition active.

  • Le mot init est-il correct ? init en tant que modificateur suffixe du constructeur, pourrait interférer si nous voulons un jour le réutiliser pour les fabriques et aussi permettre des méthodes init avec un modificateur préfixe. Autres possibilités :
    • set
  • Est-ce que required est le bon modificateur pour spécifier que tous les membres sont initialisés ? D’autres ont suggéré :
    • default
    • all
    • Avec un ! pour indiquer une logique complexe
  • Devons-nous exiger un séparateur entre base/this et init ?
    • Séparateur :
    • Séparateur ','
  • Est-ce que required est le bon modificateur ? D’autres alternatives ont été suggérées :
    • req
    • require
    • mustinit
    • must
    • explicit

Conclusion : Nous avons retiré la clause de constructeur init pour le moment et poursuivons avec required en tant que modificateur de propriété.

Restrictions des clauses init

La fonctionnalité de clause init n’a pas été implémentée dans C# 11. Elle reste une proposition active.

Devons-nous autoriser l’accès à this dans la clause init ? Si nous voulons que l’affectation dans init soit une abréviation pour affecter le membre dans le constructeur, il semble que nous devrions le permettre.

En outre, est-ce que cela crée une nouvelle étendue, comme le fait base(), ou cela partage-t-il la même étendue que le corps de la méthode ? Cela est particulièrement important pour les éléments tels que les fonctions locales, auxquelles la clause init peut souhaiter accéder, ou pour le masquage de noms, si une expression init introduit une variable via le paramètre out.

Conclusion : la clause init a été supprimée.

Exigences d’accessibilité et init.

La fonctionnalité de clause init n’a pas été implémentée dans C# 11. Elle reste une proposition active.

Dans les versions de cette proposition avec la clause init, nous avons parlé de la possibilité d’avoir le scénario suivant :

public class Base
{
    protected required int _field;

    protected Base() {} // Contract required that _field is set
}
public class Derived : Base
{
    public Derived() : init(_field = 1) // Contract is fulfilled and _field is removed from the required members list
    {
    }
}

Toutefois, nous avons supprimé la clause init de la proposition à ce stade. Nous devons donc décider s’il faut autoriser ce scénario de manière limitée. Les options que nous avons sont les suivantes :

  1. Interdire le scénario. Il s’agit de l’approche la plus conservatrice et les règles de la section Accessibilité sont actuellement écrites avec cette hypothèse à l’esprit. La règle est que tout membre requis doit être au moins aussi visible que le type qui le contient.
  2. Exiger que tous les constructeurs correspondent à l’une des options suivantes :
    1. Pas plus visible que le membre requis le moins visible.
    2. Appliquer le SetsRequiredMembersAttribute au constructeur. Ceux-ci s’assureraient que toute personne qui peut voir un constructeur peut définir tous les éléments qu’il exporte, ou bien qu'il n'y ait rien à définir. Cela peut être utile pour les types qui ne sont créés que via des méthodes Create statiques ou des constructeurs similaires, mais l'utilité semble globalement limitée.
  3. Réintroduire un moyen de retirer des parties spécifiques du contrat à la proposition, comme discuté précédemment dans LDM.

Conclusion : Option 1, tous les membres requis doivent être au moins aussi visibles que leur type contenant.

Remplacement de règles

La spec actuelle indique que le mot-clé required doit être copié et que les surcharges peuvent rendre un membre plus requis, mais pas moins. Est-ce ce que nous voulons faire ? Permettre la suppression des exigences nécessite plus de capacités de modification de contrat que ce que nous proposons actuellement.

Conclusion: L'ajout de required en dérogation est autorisé. Si l'élément remplacé est required, l'élément remplacé doit également être required.

Représentation alternative des métadonnées

Nous pourrions également adopter une approche différente de la représentation des métadonnées, en nous inspirant des méthodes d'extension. Nous pourrions ajouter un RequiredMemberAttribute sur le type pour indiquer que le type contient des membres requis, puis ajouter un RequiredMemberAttribute sur chaque membre requis. Cela simplifierait la séquence de recherche (pas besoin d’effectuer une recherche de membre, il suffit de rechercher les membres avec l’attribut).

Conclusion : alternative approuvée.

Représentation des métadonnées

La représentation des métadonnées doit être approuvée. Nous devons également décider si ces attributs doivent être inclus dans la BCL.

  1. Pour RequiredMemberAttribute, cet attribut est plus similaire aux attributs incorporés généraux que nous utilisons pour les noms de membres nullable/nint/tuple et ne sera pas appliqué manuellement par l’utilisateur en C#. Toutefois, il est possible que d’autres langages souhaitent appliquer manuellement cet attribut.
  2. SetsRequiredMembersAttribute, d’autre part, est directement utilisé par les consommateurs et devrait donc être dans la BCL.

Avec la représentation alternative évoquée dans la section précédente, cela pourrait changer l’approche sur RequiredMemberAttribute : au lieu de ressembler aux attributs intégrés pour nint/nullable/noms_de_membres_tuple, cela se rapproche de System.Runtime.CompilerServices.ExtensionAttribute, qui existe dans le framework depuis l’introduction des méthodes d’extension.

Conclusion : Nous ajouterons les deux attributs dans le BCL.

Avertissement ou erreur

Le fait de ne pas définir un membre requis doit-il être considéré comme un avertissement ou une erreur ? Il est toujours possible de contourner le système, via Activator.CreateInstance(typeof(C)) ou équivalent, ce qui signifie que nous ne pouvons pas garantir que toutes les propriétés seront toujours définies. Nous autorisons cependant la suppression des diagnostics au niveau du constructeur à l’aide du !, ce que nous n’autorisons généralement pas pour les erreurs. Cependant, cette fonctionnalité est similaire aux champs readonly ou propriétés init, car une erreur ferme est déclenchée si les utilisateurs tentent de modifier un tel membre après l’initialisation, bien que cela puisse être contourné par réflexion.

Conclusion : erreurs.