Partage via


Constructeurs principaux

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

Résumé

Les classes et les structs peuvent avoir une liste de paramètres, et leur spécification de classe de base peut avoir une liste d’arguments. Les paramètres du constructeur principal sont à portée dans toute la déclaration de classe ou de struct, et s'ils sont capturés par un membre fonctionnel ou une fonction anonyme, ils sont stockés de manière appropriée (par exemple, comme des champs privés indisables de la classe ou du struct déclaré).

La proposition « rétablit » les constructeurs primaires déjà disponibles sur les enregistrements en termes de cette fonctionnalité plus générale avec quelques membres supplémentaires synthétisés.

Motivation

La capacité d’une classe ou d’un struct en C# à avoir plusieurs constructeurs offre une certaine généralité, mais au détriment de certains désagréments dans la syntaxe de déclaration, car les paramètres du constructeur et l’état de la classe doivent être séparés correctement.

Les constructeurs principaux placent les paramètres d’un constructeur dans la portée de la classe ou du struct entier à utiliser pour l’initialisation ou directement comme état d’objet. En contrepartie, tous les autres constructeurs doivent passer par le constructeur primaire.

public class B(bool b) { } // base class

public class C(bool b, int i, string s) : B(b) // b passed to base constructor
{
    public int I { get; set; } = i; // i used for initialization
    public string S // s used directly in function members
    {
        get => s;
        set => s = value ?? throw new ArgumentNullException(nameof(S));
    }
    public C(string s) : this(true, 0, s) { } // must call this(...)
}

Conception détaillée

Cela décrit la conception généralisée couvrant à la fois les enregistrements et les non-enregistrements, puis précise comment les constructeurs principaux existants pour les enregistrements sont spécifiés en ajoutant un ensemble de membres synthétisés lorsqu'un constructeur principal est présent.

Syntaxe

Les déclarations de classe et de struct sont augmentées pour autoriser une liste de paramètres sur le nom de type, une liste d’arguments sur la classe de base et un corps composé d’un seul ;:

class_declaration
  : attributes? class_modifier* 'partial'? class_designator identifier type_parameter_list?
  parameter_list? class_base? type_parameter_constraints_clause* class_body
  ;
  
class_designator
  : 'record' 'class'?
  | 'class'
  
class_base
  : ':' class_type argument_list?
  | ':' interface_type_list
  | ':' class_type  argument_list? ',' interface_type_list
  ;  
  
class_body
  : '{' class_member_declaration* '}' ';'?
  | ';'
  ;
  
struct_declaration
  : attributes? struct_modifier* 'partial'? 'record'? 'struct' identifier type_parameter_list?
    parameter_list? struct_interfaces? type_parameter_constraints_clause* struct_body
  ;

struct_body
  : '{' struct_member_declaration* '}' ';'?
  | ';'
  ;
  
interface_declaration
  : attributes? interface_modifier* 'partial'? 'interface'
    identifier variant_type_parameter_list? interface_base?
    type_parameter_constraints_clause* interface_body
  ;  
    
interface_body
  : '{' interface_member_declaration* '}' ';'?
  | ';'
  ;

enum_declaration
  : attributes? enum_modifier* 'enum' identifier enum_base? enum_body
  ;

enum_body
  : '{' enum_member_declarations? '}' ';'?
  | '{' enum_member_declarations ',' '}' ';'?
  | ';'
  ;

Remarque : Ces productions remplacent record_declaration dans les enregistrements et record_struct_declaration dans les structures d’enregistrement , qui sont tous deux obsolètes.

Le fait qu'un class_base ait un argument_list est une erreur si le class_declaration qui l'entoure ne contient pas de parameter_list. Au plus une déclaration de type partiel d’une classe ou d’un struct partiel peut fournir un parameter_list. Les paramètres de la parameter_list d’une déclaration de record doivent tous être des paramètres de valeur.

Notez que, selon cette proposition, class_body, struct_body, interface_body et enum_body sont autorisés à se composer d’un ;.

Une classe ou un struct avec un parameter_list a un constructeur public implicite dont la signature correspond aux paramètres de valeur de la déclaration de type. Il s’agit du constructeur principal pour le type, entraînant la suppression du constructeur sans paramètre implicitement déclaré, si celui-ci est présent. Il s’agit d’une erreur d’avoir un constructeur principal et un constructeur avec la même signature déjà présente dans la déclaration de type.

Lookup

La recherche de noms simples est augmentée pour gérer les paramètres des constructeurs primaires. Les modifications sont mises en surbrillance dans en gras dans l’extrait suivant :

  • Sinon, pour chaque type d’instance T (§15.3.2), en commençant par le type d’instance de la déclaration de type englobante immédiatement et en continuant avec le type d’instance de chaque classe ou déclaration de struct englobante (le cas échéant) :
    • Si la déclaration de T inclut un paramètre de constructeur principal I et que la référence se produit dans le argument_list de T's class_base ou dans un initialiseur d’un champ, propriété ou événement de T, le résultat est le paramètre du constructeur principal I
    • Sinon, si e est égal à zéro et que la déclaration de T inclut un paramètre de type portant le nom I, le simple_name fait référence à ce paramètre de type.
    • Sinon, si une recherche membre (§12.5) de I dans T avec des arguments de type e produit une correspondance :
      • Si T est le type d’instance de la classe ou de la structure englobante immédiate et que la recherche identifie une ou plusieurs méthodes, le résultat est un groupe de méthodes avec une expression associée d’instance de this. Si une liste d’arguments de type a été spécifiée, elle est utilisée pour appeler une méthode générique (§12.8.10.2).
      • Sinon, si T est le type d’instance de la classe ou du struct englobant immédiatement, si la recherche identifie un membre d’instance et si la référence se produit dans le bloc d’un constructeur d’instance, d’une méthode d’instance ou d’un accesseur d’instance (§12.2.1), le résultat est le même qu’un accès membre (§12.8.7) du formulaire this.I. Cela ne peut se produire que lorsque e est égal à zéro.
      • Sinon, le résultat est le même qu’un accès membre (§12.8.7) du formulaire T.I ou T.I<A₁, ..., Aₑ>.
    • Sinon, si la déclaration de T inclut un paramètre de constructeur principal I, le résultat est le paramètre de constructeur principal I.

Le premier ajout correspond à la modification engendrée par les constructeurs principaux sur les enregistrements, et garantit que les paramètres du constructeur principal sont trouvés avant les champs correspondants dans les initialiseurs et les arguments de classe de base. Elle étend également cette règle aux initialiseurs statiques. Toutefois, étant donné que les enregistrements ont toujours un membre d’instance portant le même nom que le paramètre, l’extension ne peut entraîner qu’une modification dans un message d’erreur. Accès illégal à un paramètre par rapport à un accès illégal à un membre d’instance.

Le deuxième ajout permet aux paramètres du constructeur primaire d'être trouvés ailleurs dans le corps du type, mais seulement s'ils ne sont pas masqués par des membres.

C'est une erreur de référencer un paramètre de constructeur principal si la référence ne se produit pas dans l'une des situations suivantes :

  • un argument nameof
  • un initialisateur d'un champ d'instance, d'une propriété ou d'un événement du type déclarant (type déclarant le constructeur primaire avec le paramètre).
  • le argument_list de class_base du type déclarant.
  • le corps d'une méthode d'instance (notez que les constructeurs d'instance sont exclus) du type déclarant.
  • le corps d'un accesseur d'instance du type déclarant.

En d'autres termes, les paramètres du constructeur primaire ont une étendue sur tout le corps du type de déclaration. Ils font de l'ombre aux membres du type déclarant dans un initialisateur d'un champ, d'une propriété ou d'un événement du type déclarant, ou dans le argument_list de class_base du type déclarant. Partout ailleurs, les membres du type déclarant leur font de l'ombre.

Par conséquent, dans la déclaration suivante :

class C(int i)
{
    protected int i = i; // references parameter
    public int I => i; // references field
}

L’initialiseur du champ i fait référence au paramètre i, tandis que le corps de la propriété I fait référence au champ i.

Avertissement sur l'observation par un membre de la base

Le compilateur génère un avertissement sur l’utilisation d’un identificateur lorsqu’un membre de base ombre un paramètre de constructeur principal si ce paramètre de constructeur principal n’a pas été passé au type de base via son constructeur.

Un paramètre de constructeur principal est considéré comme transmis au type de base via son constructeur lorsque toutes les conditions suivantes sont vraies pour un argument dans class_base:

  • L’argument représente une conversion d’identité implicite ou explicite d’un paramètre de constructeur principal ;
  • L’argument ne fait pas partie d’un argument params développé ;

Sémantique

Un constructeur primaire entraîne la génération d'un constructeur d'instance sur le type englobant avec les paramètres donnés. Si le class_base a une liste d’arguments, le constructeur d’instance généré aura un initialiseur base avec la même liste d’arguments.

Les paramètres du constructeur principal dans les déclarations de classe/struct peuvent être déclarés ref, in ou out. La déclaration de paramètres ref ou out reste illégale dans les constructeurs primaires de la déclaration d'enregistrement.

Tous les initialisateurs de membres d'instance dans le corps de la classe deviendront des affectations dans le constructeur généré.

Si un paramètre de constructeur principal est référencé à partir d’un membre d’instance et que la référence n’est pas à l’intérieur d’un argument nameof, elle est capturée dans l’état du type englobant, de sorte qu’elle reste accessible après l’arrêt du constructeur. Une stratégie de mise en œuvre probable consiste à utiliser un champ privé dont le nom est modifié. Dans une structure en lecture seule, les champs de capture seront en lecture seule. Par conséquent, l’accès aux paramètres capturés d’un struct en lecture seule aura des restrictions similaires à l’accès aux champs en lecture seule. L’accès aux paramètres capturés au sein d’un membre en lecture seule aura des restrictions similaires à l’accès aux champs d’instance dans le même contexte.

La capture n’est pas autorisée pour les paramètres qui ont un type similaire à ref, et la capture n’est pas autorisée pour les paramètres ref, in ou out. Ceci est similaire à une limitation de la capture dans les lambdas.

Si un paramètre de constructeur principal est référencé uniquement à partir des initialiseurs membres de l’instance, ceux-ci peuvent directement référencer le paramètre du constructeur généré, car ils sont exécutés dans le cadre de celui-ci.

Le constructeur principal effectue la séquence d’opérations suivante :

  1. Les valeurs de paramètre sont stockées dans les champs de capture, le cas échéant.
  2. Les initialiseurs d’instance sont exécutés
  3. L'initialisateur du constructeur de base est appelé

Les références de paramètre dans n’importe quel code utilisateur sont remplacées par les références de champ de capture correspondantes.

Par exemple, cette déclaration :

public class C(bool b, int i, string s) : B(b) // b passed to base constructor
{
    public int I { get; set; } = i; // i used for initialization
    public string S // s used directly in function members
    {
        get => s;
        set => s = value ?? throw new ArgumentNullException(nameof(value));
    }
    public C(string s) : this(true, 0, s) { } // must call this(...)
}

Génère du code similaire à ce qui suit :

public class C : B
{
    public int I { get; set; }
    public string S
    {
        get => __s;
        set => __s = value ?? throw new ArgumentNullException(nameof(value));
    }
    public C(string s) : this(0, s) { ... } // must call this(...)
    
    // generated members
    private string __s; // for capture of s
    public C(bool b, int i, string s)
    {
        __s = s; // capture s
        I = i; // run I's initializer
        B(b) // run B's constructor
    }
}

Il est erroné qu'une déclaration de constructeur non primaire ait la même liste de paramètres que le constructeur principal. Toutes les déclarations de constructeur non primaire doivent utiliser un initialiseur this, afin que le constructeur principal soit finalement appelé.

Les enregistrements produisent un avertissement si un paramètre de constructeur principal n’est pas lu dans les initialiseurs d’instance (éventuellement générés) ou l’initialiseur de base. Des avertissements similaires sont signalés pour les paramètres de constructeur principal dans les classes et les structures :

  • pour un paramètre passé par valeur, si le paramètre n'est pas capturé et n'est pas lu dans les initialiseurs d'instance ni dans l'initialiseur de base.
  • pour un paramètre in, si le paramètre n’est pas lu dans des initialiseurs d’instance ou un initialiseur de base.
  • pour un paramètre ref, si le paramètre n’est pas lu ou écrit dans n’importe quel initialiseur d’instance ou initialiseur de base.

Noms simples identiques et noms de types

Il existe une règle de langage spéciale pour les scénarios souvent appelés scénarios « Color Color » - noms simples et noms de type identiques.

Dans un accès membre du formulaire E.I, si E est un identificateur unique, et si la signification de E en tant que simple_name (§12.8.4) est une constante, un champ, une propriété, une variable locale ou un paramètre ayant le même type que la signification de E en tant que type_name (§7.8.1), alors les deux significations possibles de E sont autorisées. La recherche de membres de E.I n’est jamais ambiguë, car I doit nécessairement être membre du type E dans les deux cas. En d'autres termes, la règle permet simplement l'accès aux membres statiques et aux types imbriqués de E lorsqu'une erreur de compile-time se serait autrement produite.

En ce qui concerne les constructeurs principaux, la règle affecte si un identificateur au sein d’un membre d’instance doit être traité comme une référence de type ou en tant que référence de paramètre de constructeur principal, qui, à son tour, capture le paramètre dans l’état du type englobant. Même si « la recherche de membre de E.I n’est jamais ambiguë », lorsque la recherche génère un groupe de membres, dans certains cas, il est impossible de déterminer si un accès membre fait référence à un membre statique ou à un membre d’instance sans résoudre complètement (lier) l'accès au membre. En même temps, la capture d’un paramètre de constructeur principal modifie les propriétés d’un type englobant d’une manière qui affecte l’analyse sémantique. Par exemple, le type pourrait devenir non géré et échouer à certaines contraintes pour cette raison. Il existe même des scénarios pour lesquels la liaison peut réussir dans un sens ou dans l'autre, selon que le paramètre est considéré comme capturé ou non. Par exemple:

struct S1(Color Color)
{
    public void Test()
    {
        Color.M1(this); // Error: ambiguity between parameter and typename
    }
}

class Color
{
    public void M1<T>(T x, int y = 0)
    {
        System.Console.WriteLine("instance");
    }
    
    public static void M1<T>(T x) where T : unmanaged
    {
        System.Console.WriteLine("static");
    }
}

Si nous traitons le récepteur Color comme une valeur, nous capturez le paramètre et « S1 » devient géré. Ensuite, la méthode statique devient inapplicable en raison de la contrainte et nous appelons la méthode d’instance. Toutefois, si nous traitons le récepteur comme un type, nous ne capturez pas le paramètre et « S1 » reste non managé, alors les deux méthodes sont applicables, mais la méthode statique est « meilleure », car elle n’a pas de paramètre facultatif. Aucun des choix n’entraîne une erreur, mais chacun entraîne un comportement distinct.

Étant donné cela, le compilateur génère une erreur d’ambiguïté pour un accès membre E.I lorsque toutes les conditions suivantes sont remplies :

  • La recherche de membres de E.I génère un groupe de membres contenant des membres d’instance et statiques en même temps. Les méthodes d’extension applicables au type de récepteur sont traitées comme des méthodes d’instance à des fins de vérification.
  • Si E est traité comme un nom simple, plutôt qu’un nom de type, il fait référence à un paramètre de constructeur principal et capture le paramètre dans l’état du type englobant.

Avertissements de stockage double

Si un paramètre de constructeur principal est passé à la base et également capturé, il existe un risque élevé qu’il est stocké par inadvertance deux fois dans l’objet.

Le compilateur génère un avertissement pour in ou par argument valeur dans un class_baseargument_list lorsque toutes les conditions suivantes sont remplies :

  • L’argument représente une conversion d’identité implicite ou explicite d’un paramètre de constructeur principal ;
  • L’argument ne fait pas partie d’un argument params développé ;
  • Le paramètre principal du constructeur est capturé dans l'état du type englobant.

Le compilateur génère un avertissement pour un variable_initializer lorsque toutes les conditions suivantes sont remplies :

  • L’initialiseur de variable représente une conversion d’identité implicite ou explicite d’un paramètre de constructeur principal ;
  • Le paramètre principal du constructeur est capturé dans l'état du type englobant.

Par exemple:

public class Person(string name)
{
    public string Name { get; set; } = name;   // warning: initialization
    public override string ToString() => name; // capture
}

Attributs ciblant les constructeurs principaux

À https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-03-13.md nous avons décidé d’adopter la proposition de https://github.com/dotnet/csharplang/issues/7047.

La cible de l'attribut « method » est autorisée sur une class_declaration/struct_declaration avec parameter_list et a pour résultat que le constructeur primaire correspondant possède cet attribut. Les attributs avec la cible method sur une class_declaration/struct_declaration sans parameter_list sont ignorés avec un avertissement.

[method: FooAttr] // Good
public partial record Rec(
    [property: Foo] int X,
    [field: NonSerialized] int Y
);
[method: BarAttr] // warning CS0657: 'method' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'type'. All attributes in this block will be ignored.
public partial record Rec
{
    public void Frobnicate()
    {
        ...
    }
}
[method: Attr] // Good
public record MyUnit1();
[method: Attr] // warning CS0657: 'method' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'type'. All attributes in this block will be ignored.
public record MyUnit2;

Constructeurs primaires sur les enregistrements

Avec cette proposition, les enregistrements n’ont plus besoin de spécifier séparément un mécanisme de constructeur principal. Au lieu de cela, les déclarations d’enregistrement (classe et struct) qui ont des constructeurs principaux suivraient les règles générales, avec ces ajouts simples :

  • Pour chaque paramètre de constructeur principal, si un membre portant le même nom existe déjà, il doit s’agir d’une propriété ou d’un champ d’instance. Si ce n'est pas le cas, une propriété automatique publique init-only du même nom est synthétisée avec un initialisateur de propriété affectant à partir du paramètre.
  • Un déconstructeur est synthétisé avec des paramètres out correspondant aux paramètres du constructeur primaire.
  • Si une déclaration explicite de constructeur est un « constructeur de copie » - un constructeur qui prend un seul paramètre du type englobant - il n'est pas nécessaire d'appeler un initialisateur this et n'exécutera pas les initialisateurs de membres présents dans la déclaration d'enregistrement.

Inconvénients

  • La taille d’allocation des objets construits est moins évidente, car le compilateur détermine s’il faut allouer un champ pour un paramètre de constructeur principal en fonction du texte intégral de la classe. Ce risque est similaire à la capture implicite de variables par expressions lambda.
  • Une tentation courante (ou modèle accidentel) peut être de capturer le paramètre « identique » à plusieurs niveaux d’héritage, car il est transmis à la chaîne de constructeur au lieu de lui attribuer explicitement un champ protégé à la classe de base, ce qui entraîne des allocations dupliquées pour les mêmes données dans les objets. Cela ressemble fortement au risque actuel de remplacer des propriétés automatiques par d'autres propriétés automatiques.
  • Tel qu'il est proposé ici, il n'y a pas de place pour une logique supplémentaire qui pourrait être exprimée dans les corps des constructeurs. L'extension « corps de constructeur primaire » ci-dessous y remédie.
  • Telle que proposée, la sémantique de l'ordre d'exécution est subtilement différente de celle des constructeurs ordinaires, retardant les initialisateurs de membres après les appels de base. Il est probablement possible d'y remédier, mais au détriment de certaines propositions d'extension (notamment « primary constructor bodies »).
  • La proposition fonctionne uniquement pour les scénarios où un seul constructeur peut être désigné comme principal.
  • Il n’existe aucun moyen d’exprimer l’accessibilité distincte de la classe et du constructeur principal. Par exemple, lorsque les constructeurs publics sont tous délégués à un constructeur privé « build-it-all ». Si nécessaire, la syntaxe peut être proposée ultérieurement.

Alternatives

Aucune capture

Une version beaucoup plus simple de cette fonctionnalité interdirait aux paramètres des constructeurs primaires de figurer dans les corps des membres. Les référencer serait une erreur. Les champs doivent être déclarés explicitement si le stockage est souhaité au-delà du code d’initialisation.

public class C(string s)
{
    public string S1 => s; // Nope!
    public string S2 { get; } = s; // Still allowed
}

Cela pourrait encore évoluer vers la proposition complète à un moment ultérieur, et éviterait un certain nombre de décisions et de complexités, au coût de supprimer moins de contenu initialement, et donnerait probablement aussi l'impression d'être contre-intuitif.

Champs générés explicitement

Une autre approche consiste à ce que les paramètres du constructeur principal génèrent toujours et visiblement un champ du même nom. Au lieu de fermer les paramètres de la même manière que les fonctions locales et anonymes, il y aurait explicitement une déclaration de membre générée, similaire aux propriétés publiques générées pour les paramètres de constructeur primaire dans les enregistrements. Comme pour les enregistrements, si un membre approprié existe déjà, il ne serait pas généré.

Si le champ généré est privé, il peut toujours être supprimé lorsqu’il n’est pas utilisé comme champ dans les corps membres. Dans les classes, cependant, un champ privé n'est souvent pas le bon choix, en raison de la duplication d'état qu'il pourrait entraîner dans les classes dérivées. Une option ici consisterait à générer plutôt un champ protégé dans les classes, ce qui encourage la réutilisation du stockage entre les couches d’héritage. Toutefois, nous ne pourrions pas omettre la déclaration, et nous entraînerions un coût d’allocation pour chaque paramètre de constructeur principal.

Cela permettrait d'aligner plus étroitement les constructeurs primaires non enregistrés sur ceux des enregistrements, en ce sens que les membres sont toujours (au moins conceptuellement) générés, même s'il s'agit de différents types de membres avec des accessibilités différentes. Mais cela entraînerait également des différences surprenantes par rapport à la façon dont les paramètres et les locaux sont capturés ailleurs en C#. Si jamais nous autorisions des classes locales, par exemple, elles captureraient implicitement les paramètres et les variables locales englobants. La génération visible de champs d’ombre pour eux ne semble pas être un comportement raisonnable.

Un autre problème souvent soulevé avec cette approche est que de nombreux développeurs ont des conventions d’affectation de noms différentes pour les paramètres et les champs. Qui doit être utilisé pour le paramètre du constructeur principal ? L’un ou l’autre choix entraînerait une incohérence avec le reste du code.

Enfin, la génération visible des déclarations de membres est vraiment la règle du jeu pour les enregistrements, mais elle est beaucoup plus surprenante et « hors norme » pour les classes et les structures non enregistrées. Dans tous les cas, ce sont les raisons pour lesquelles la proposition principale opte pour la capture implicite, avec un comportement sensible (cohérent avec les enregistrements) pour les déclarations de membre explicites quand elles sont souhaitées.

Supprimer les membres d’instance de la portée de l’initialiseur

Les règles de recherche ci-dessus sont destinées à permettre le comportement actuel des paramètres du constructeur primaire dans les enregistrements lorsqu’un membre correspondant est déclaré manuellement, et expliquer le comportement du membre généré lorsqu’il ne l’est pas. Il faut pour cela que la recherche diffère entre « l'étendue de l'initialisation » (initialisateurs this/base, initialisateurs de membres) et « l'étendue du corps » (corps des membres), ce que la proposition ci-dessus permet de faire en changeant le moment où les paramètres du constructeur primaire sont recherchés, en fonction de l'endroit où la référence se produit.

Il est à noter que le référencement d'un membre d'instance avec un nom simple dans le contexte de l'initialiseur entraîne toujours une erreur. Au lieu de simplement faire de l'ombre aux membres de l'instance à ces endroits, pourrions-nous simplement les retirer de l'étendue ? De cette façon, il n'y aurait pas cet étrange ordre conditionnel des étendues.

Cette alternative est probablement possible, mais elle aurait des conséquences quelque peu éloignées et potentiellement indésirables. Tout d'abord, si nous retirons les membres d'instance de l'étendue de l'initialisateur, un simple nom qui correspond à un membre d'instance et non à un paramètre de constructeur primaire pourrait accidentellement se lier à quelque chose en dehors de la déclaration de type ! Cela semble être rarement intentionnel, et une erreur serait meilleure.

En outre, les membres statiques peuvent être référencés dans l'étendue de l'initialisation. Nous devons donc faire la distinction entre les membres statiques et les membres d’instance dans la recherche, ce que nous ne faisons pas aujourd’hui. (Nous faisons la distinction dans la résolution de surcharge, mais ce n’est pas en jeu ici). Il faudrait donc changer cela aussi, ce qui conduirait à d'autres situations où, par exemple, dans des contextes statiques, quelque chose se lierait « plus loin » plutôt que de commettre une erreur parce qu'il a trouvé un membre d'instance.

Toutes ces « simplifications » mèneraient à une complication en aval que personne n’a demandé.

Extensions possibles

Il s’agit de variantes ou d’ajouts à la proposition de base qui peuvent être prises en considération conjointement avec elle, ou à un stade ultérieur, s’ils sont jugés utiles.

Accès aux paramètres du constructeur principal dans les constructeurs

Les règles ci-dessus rendent erroné le fait de référencer un paramètre de constructeur principal au sein d’un autre constructeur. Cela pourrait être autorisé dans le corps d'autres constructeurs, cependant, puisque le constructeur primaire s'exécute en premier. Toutefois, il devrait rester interdit dans la liste d’arguments de l’initialiseur this.

public class C(bool b, int i, string s) : B(b)
{
    public C(string s) : this(b, s) // b still disallowed
    { 
        i++; // could be allowed
    }
}

Un tel accès serait toujours soumis à la capture, car ce serait la seule façon pour le corps du constructeur d'accéder à la variable après que le constructeur primaire se soit déjà exécuté.

L'interdiction des paramètres du constructeur principal dans les arguments de cet initialiseur pourrait être assouplie pour les autoriser, mais les rendre non définitivement affectés, ce qui ne paraît cependant pas utile.

Autoriser les constructeurs sans initialisateur this

Les constructeurs sans initialiseur this (par exemple, avec un initialiseur implicite ou explicite base) pourraient être autorisés. Un tel constructeur n'exécuterait pas les initialisateurs de champ d'instance, de propriété et d'événement, car ceux-ci seraient considérés comme faisant partie du constructeur primaire uniquement.

En présence de tels constructeurs appelants de base, il existe quelques options pour la façon dont la capture des paramètres du constructeur principal est gérée. Le plus simple est de interdire complètement la capture dans cette situation. Les paramètres de constructeur principal sont destinés à l’initialisation uniquement lorsque de tels constructeurs existent.

Sinon, s’ils sont combinés avec l’option décrite précédemment pour autoriser l’accès aux paramètres du constructeur principal au sein des constructeurs, les paramètres peuvent entrer dans le corps du constructeur tel qu’ils ne sont pas définitivement attribués, et ceux qui sont capturés doivent être définitivement affectés par la fin du corps du constructeur. Il s'agirait essentiellement de paramètres de sortie implicites. De cette façon, les paramètres de constructeur principal capturés auraient toujours une valeur sensible (c’est-à-dire explicitement affectée) au moment où ils sont consommés par d’autres membres de fonction.

Une attraction de cette extension (dans les deux formes) est qu’elle généralise entièrement l’exemption actuelle pour les « constructeurs de copie » dans les enregistrements, sans entraîner de situations où les paramètres de constructeur principal non initialisés sont observés. Essentiellement, les constructeurs qui initialisent l’objet de manière alternative sont corrects. Les restrictions liées à la capture ne constitueraient pas un changement radical pour les constructeurs de copie définis manuellement dans les enregistrements, car les enregistrements ne capturent jamais les paramètres de leur constructeur primaire (ils génèrent des champs à la place).

public class C(bool b, int i, string s) : B(b)
{
    public int I { get; set; } = i; // i used for initialization
    public string S // s used directly in function members
    {
        get => s;
        set => s = value ?? throw new ArgumentNullException(nameof(value));
    }
    public C(string s2) : base(true) // cannot use `string s` because it would shadow
    { 
        s = s2; // must initialize s because it is captured by S
    }
    protected C(C original) : base(original) // copy constructor
    {
        this.s = original.s; // assignment to b and i not required because not captured
    }
}

Corps des constructeurs primaires

Les constructeurs eux-mêmes contiennent souvent une logique de validation de paramètre ou un autre code d’initialisation nontrivial qui ne peut pas être exprimé en tant qu’initialiseurs.

Les constructeurs primaires pourraient être étendus pour permettre aux blocs d'instruction d'apparaître directement dans le corps de la classe. Ces instructions seraient insérées dans le constructeur généré au moment où elles apparaissent entre les affectations d'initialisation, et seraient donc exécutées entre les initialisations. Par exemple:

public class C(int i, string s) : B(s)
{
    {
        if (i < 0) throw new ArgumentOutOfRangeException(nameof(i));
    }
	int[] a = new int[i];
    public int S => s;
}

Une grande partie de ce scénario pourrait être couverte de manière adéquate si nous introduisions des « initialisateurs finaux » qui s'exécutent après que les constructeurs et tous les initialisateurs d'objets/de collections ont été exécutés. Toutefois, la validation des arguments est une chose qui se produirait idéalement dès que possible.

Les corps des constructeurs primaires pourraient également fournir un endroit pour autoriser un modificateur d'accès pour le constructeur primaire, lui permettant de s'écarter de l'accessibilité du type qui l'entoure.

Déclarations de paramètre et de membre combinées

Un ajout possible et souvent mentionné pourrait être de permettre aux paramètres du constructeur primaire d'être annotés de manière à ce qu'ils déclarent également un membre du type. Le plus souvent, il est proposé de permettre à un spécificateur d'accès sur les paramètres de déclencher la génération d'un membre :

public class C(bool b, protected int i, string s) : B(b) // i is a field as well as a parameter
{
    void M()
    {
        ... i ... // refers to the field i
        ... s ... // closes over the parameter s
    }
}

Il existe des problèmes :

  • Que se passe-t-il si une propriété est souhaitée, et non un champ ? Avoir la syntaxe { get; set; } en ligne dans une liste de paramètres ne semble pas très appétissant.
  • Que se passe-t-il si différentes conventions d’affectation de noms sont utilisées pour les paramètres et les champs ? Alors cette fonctionnalité serait inutile.

Il s’agit d’un ajout potentiel qui peut être adopté ou non. La proposition actuelle laisse la possibilité ouverte.

Questions ouvertes

Ordre de recherche pour les paramètres de type

La section https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/primary-constructors.md#lookup spécifie que les paramètres de type de l'entité déclarée doivent se présenter avant les paramètres du constructeur principal du type dans chaque contexte où ces paramètres sont dans le champ d'application. Toutefois, nous avons déjà un comportement existant avec des enregistrements : les paramètres du constructeur principal sont fournis avant les paramètres de type dans les initialiseurs de base et les initialiseurs de champs.

Que devrions-nous faire à propos de cette différence ?

  • Ajustez les règles pour qu’elles correspondent au comportement.
  • Ajustez le comportement (un changement de rupture possible).
  • Interdire à un paramètre de constructeur primaire d'utiliser le nom du paramètre de type (une modification susceptible de rompre la compatibilité).
  • Ne rien faire, acceptez l’incohérence entre la spécification et l’implémentation.

Conclusion:

Ajustez les règles pour qu’elles correspondent au comportement (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-09-25.md#primary-constructors).

Attributs de ciblage de champ pour les paramètres de constructeur principal capturés

Devons-nous autoriser les attributs de ciblage de champ pour les paramètres de constructeur principal capturés ?

class C1([field: Test] int x) // Parameter is captured, the attribute goes to the capture field
{
    public int X => x;
}
class C2([field: Test] int x) // Parameter is not captured, the attribute is ignored with a warning CS0657: 'field' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'param'. All attributes in this block will be ignored.
{
    public int X = x;
}

À l'heure actuelle, les attributs sont ignorés, et un avertissement est émis, que le paramètre soit capturé ou non.

Notez que pour les enregistrements, les attributs spécifiques aux champs sont autorisés lorsqu'une propriété est synthétisée pour cette dernière. Les attributs sont alors placés dans le champ d'appui.

record R1([field: Test]int X); // Ok, the attribute goes on the backing field
record R2([field: Test]int X) // warning CS0657: 'field' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'param'. All attributes in this block will be ignored.
{
    public int X = X;
}

Conclusion:

Non autorisé (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-03.md#attributes-on-captured-parameters).

Avertissement sur l'observation par un membre de la base

Devrions-nous signaler un avertissement lorsqu'un membre de la base suit un paramètre de constructeur primaire à l'intérieur d'un membre (voir https://github.com/dotnet/csharplang/discussions/7109#discussioncomment-5666621) ?

Conclusion:

Une autre conception est approuvée - https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-08.md#primary-constructors

Capturer l'instance du type englobant dans une fermeture

Lorsqu’un paramètre capturé dans l’état du type englobant est également référencé dans une expression lambda à l’intérieur d’un initialiseur d’instance ou d’un initialiseur de base, le lambda et l’état du type englobant doivent faire référence au même emplacement pour le paramètre. Par exemple:

partial class C1
{
    public System.Func<int> F1 = Execute1(() => p1++);
}

partial class C1 (int p1)
{
    public int M1() { return p1++; }
    static System.Func<int> Execute1(System.Func<int> f)
    {
        _ = f();
        return f;
    }
}

Étant donné que l’implémentation naïve de capture d’un paramètre dans l’état du type capture simplement le paramètre dans un champ d’instance privée, l’lambda doit faire référence au même champ. Par conséquent, il doit être en mesure d’accéder à l’instance du type. Cela nécessite de capturer this dans une fermeture avant que le constructeur de base ne soit invoqué. Cela, à son tour, entraîne un IL sûr, mais invérifiable. Est-ce acceptable ?

Nous pourrions également :

  • Interdisez les lambdas de ce type ;
  • Vous pouvez également capturer des paramètres comme celui d’une instance d’une classe distincte (une autre fermeture) et partager cette instance entre la fermeture et l’instance du type englobant. Il n'est donc plus nécessaire de capturer this dans une fermeture.

Conclusion:

Nous sommes satisfaits de la capture de this dans une fermeture avant l'invocation du constructeur de base (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md). L’équipe runtime n’a pas également trouvé le modèle IL problématique.

Assignation à this à l'intérieur d'une structure

C# permet d'assigner this à l'intérieur d'une structure. Si le struct capture un paramètre de constructeur principal, l’affectation va remplacer sa valeur, ce qui peut ne pas être évident pour l’utilisateur. Voulez-nous signaler un avertissement pour les affectations comme celle-ci ?

struct S(int x)
{
    int X => x;
    
    void M(S s)
    {
        this = s; // 'x' is overwritten
    }
}

Conclusion:

Autorisé, aucun avertissement (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md).

Avertissement de double stockage pour l'initialisation et la capture

Nous avons un avertissement si un paramètre de constructeur primaire est passé à la base et également capturé, parce qu'il y a un risque élevé qu'il soit stocké par inadvertance deux fois dans l'objet.

Il semble qu’il existe un risque similaire si un paramètre est utilisé pour initialiser un membre et qu’il est également capturé. Voici un petit exemple :

public class Person(string name)
{
    public string Name { get; set; } = name;   // initialization
    public override string ToString() => name; // capture
}

Pour une instance donnée de Person, les modifications apportées à Name ne seraient pas reflétées dans la sortie de ToString, ce qui est probablement inattendu de la part du développeur.

Devrions-nous introduire un double avertissement de stockage pour cette situation ?

C’est ainsi qu’il fonctionne :

Le compilateur génère un avertissement pour un variable_initializer lorsque toutes les conditions suivantes sont remplies :

  • L’initialiseur de variable représente une conversion d’identité implicite ou explicite d’un paramètre de constructeur principal ;
  • Le paramètre principal du constructeur est capturé dans l'état du type englobant.

Conclusion:

Approuvé, voir https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-15.md#primary-constructors

Réunions LDM