Partage via


mot clé field dans les propriétés

Résumé

Étendez toutes les propriétés pour leur permettre de référencer un champ de stockage généré automatiquement à l’aide du nouveau mot clé contextuel field. Les propriétés peuvent désormais contenir un accesseur sans corps ainsi qu'un accesseur avec corps.

Motivation

Les propriétés automatiques ne permettent que de définir ou d'obtenir directement le champ de stockage, en donnant un certain contrôle uniquement en plaçant des modificateurs d'accès sur les accesseurs. Parfois, il est nécessaire d’avoir un contrôle supplémentaire sur ce qui se passe dans un ou les deux accesseurs, mais cela confronte les utilisateurs à la surcharge de déclaration d’un champ de stockage. Le nom du champ de stockage doit alors être synchronisé avec la propriété, et le champ de stockage est étendu à l'ensemble de la classe, ce qui peut entraîner un contournement accidentel des accesseurs à l'intérieur de la classe.

Il existe plusieurs scénarios courants. Dans la méthode getter, il existe une initialisation tardive ou des valeurs par défaut si la propriété n’a jamais été assignée. Dans le setter, il existe une contrainte pour garantir la validité d’une valeur, ou détecter et propager des mises à jour telles que le déclenchement de l’événement INotifyPropertyChanged.PropertyChanged.

Dans ces cas, vous devez toujours créer un champ d'instance et écrire toute la propriété vous-même. Cela n'ajoute pas seulement une bonne quantité de code, mais cela laisse également échapper le champ de soutien dans le reste de l'étendue du type, alors qu'il est souvent souhaitable qu'il ne soit disponible que pour les corps des accesseurs.

Glossaire

  • Propriété Auto: abréviation de « propriété implémentée automatiquement » (§15.7.4). Les accesseurs d'une propriété auto n'ont pas de corps. L’implémentation et la mémoire de stockage sont fournies par le compilateur. Les propriétés auto ont { get; }, { get; set; } ou { get; init; }.

  • accesseur automatique: court pour « accesseur implémenté automatiquement ». Il s’agit d’un accesseur qui n’a pas de corps. L'implémentation et le stockage de sauvegarde sont fournis par le compilateur. get;, set; et init; sont des accesseurs automatiques.

  • Accesseur complet : il s'agit d'un accesseur qui possède un corps. L’implémentation n’est pas fournie par le compilateur, même si le stockage sous-jacent peut toujours être fourni (comme dans l’exemple set => field = value;).

  • Propriété adossée à un champ : il s'agit soit d'une propriété utilisant le mot-clé field dans le corps d'un accesseur, soit d'une propriété automatique.

  • Champ De stockage : il s’agit de la variable indiquée par le mot clé field dans les accesseurs d’une propriété, qui est également en lecture implicite ou écrite dans des accesseurs implémentés automatiquement (get;, set; ou init;).

Conception détaillée

Pour les propriétés avec un accesseur init, tout ce qui s’applique ci-dessous à set s’applique plutôt au accesseur init.

Il existe deux modifications de syntaxe :

  1. Il existe un nouveau mot-clé contextuel, field, qui peut être utilisé dans les corps d'accesseurs des propriétés pour accéder à un champ de stockage pour la déclaration de la propriété (décision du LDM).

  2. Les propriétés peuvent désormais combiner des accesseurs automatiques avec des accesseurs complets (décision du MLD). Le terme « propriété auto » continuera à désigner une propriété dont les accesseurs n'ont pas de corps. Aucun des exemples ci-dessous ne sera considéré comme des propriétés automatiques.

Exemples:

{ get; set => Set(ref field, value); }
{ get => field ?? parent.AmbientValue; set; }

Les deux accesseurs peuvent être des accesseurs complets, l'un ou l'autre ou les deux faisant usage de field :

{ get => field; set => field = value; }
{ get => field; set => throw new InvalidOperationException(); }
{ get => overriddenValue; set => field = value; }
{
    get;
    set
    {
        if (field == value) return;
        field = value;
        OnXyzChanged();
    }
}

Les propriétés expression-bodied et les propriétés n'ayant qu'un accesseur get peuvent également utiliser field :

public string LazilyComputed => field ??= Compute();
public string LazilyComputed { get => field ??= Compute(); }

Les propriétés définies uniquement peuvent également utiliser field:

{
    set
    {
        if (field == value) return;
        field = value;
        OnXyzChanged(new XyzEventArgs(value));
    }
}

Changements cassants

L’existence du mot clé contextuel field dans les corps d’accesseurs de propriété est un changement potentiellement disruptif.

field étant un mot-clé et non un identificateur, il ne peut être « masqué » par un identificateur qu'en utilisant la méthode normale d'écrêtage des mots-clés : @field. Tous les identificateurs nommés field déclarés dans des corps d'accesseurs de propriétés peuvent se prémunir contre les ruptures lors de la mise à niveau à partir de versions C# antérieures à la version 14 en ajoutant le @ initial.

Si une variable nommée field est déclarée dans un accesseur de propriété, une erreur est signalée.

Dans la version 14 ou supérieure du langage, un avertissement est émis si une expression primaire field fait référence au champ de stockage, mais aurait référé à un autre symbole dans une version antérieure du langage.

Attributs ciblés par champ

Comme pour les propriétés automatiques, toute propriété qui utilise un champ de stockage dans l'un de ses accesseurs pourra utiliser des attributs ciblés par champ :

[field: Xyz]
public string Name => field ??= Compute();

[field: Xyz]
public string Name { get => field; set => field = value; }

Un attribut ciblant un champ restera invalide à moins qu'un accesseur n'utilise un champ de stockage :

// ❌ Error, will not compile
[field: Xyz]
public string Name => Compute();

Initialiseurs de propriétés

Les propriétés comportant des initialisateurs pourront utiliser field. Le champ de stockage est directement initialisé au lieu d'appeler le fixateur (décision du LDM).

L’appel d’un setter pour un initialiseur n’est pas une option ; les initialiseurs sont traités avant d’appeler des constructeurs de base et il est illégal d’appeler n’importe quelle méthode d’instance avant l’appel du constructeur de base. Cela est également important pour l’initialisation par défaut/affectation définitive de structs.

Cela permet un contrôle flexible sur l’initialisation. Si vous souhaitez initialiser sans appeler le setter, vous utilisez un initialiseur de propriété. Si vous souhaitez initialiser en appelant le setter, vous attribuez une valeur initiale à la propriété dans le constructeur.

Voici un exemple de l’endroit où cela est utile. Nous pensons que le mot clé field trouvera beaucoup de son utilisation avec des modèles de vue en raison de la solution élégante qu’il apporte pour le modèle INotifyPropertyChanged. Les setters de propriétés du modèle d'affichage sont susceptibles d'être liés à l'interface utilisateur et d'entraîner le suivi des modifications ou de déclencher d'autres comportements. Le code suivant doit initialiser la valeur par défaut de IsActive sans définir HasPendingChanges sur true:

class SomeViewModel
{
    public bool HasPendingChanges { get; private set; }

    public bool IsActive { get; set => Set(ref field, value); } = true;

    private bool Set<T>(ref T location, T value)
    {
        if (RuntimeHelpers.Equals(location, value))
            return false;

        location = value;
        HasPendingChanges = true;
        return true;
    }
}

Cette différence de comportement entre un initialiseur de propriété et l’affectation à partir du constructeur peut également être vue avec des propriétés automatiques virtuelles dans les versions précédentes du langage :

using System;

// Nothing is printed; the property initializer is not
// equivalent to `this.IsActive = true`.
_ = new Derived();

class Base
{
    public virtual bool IsActive { get; set; } = true;
}

class Derived : Base
{
    public override bool IsActive
    {
        get => base.IsActive;
        set
        {
            base.IsActive = value;
            Console.WriteLine("This will not be reached");
        }
    }
}

Affectation à partir du constructeur

Comme pour les propriétés auto, l'affectation dans le constructeur appelle le setter (potentiellement virtuel) s'il existe, et s'il n'y a pas de setter, elle revient à l'affectation directe au champ de stockage.

class C
{
    public C()
    {
        P1 = 1; // Assigns P1's backing field directly
        P2 = 2; // Assigns P2's backing field directly
        P3 = 3; // Calls P3's setter
        P4 = 4; // Calls P4's setter
    }

    public int P1 => field;
    public int P2 { get => field; }
    public int P4 { get => field; set => field = value; }
    public int P3 { get => field; set; }
}

Assignation définie dans les structures

Même s'ils ne peuvent pas être référencés dans le constructeur, les champs de stockage désignés par le mot-clé field sont soumis aux avertissements default-initialization et disabled-by-default dans les mêmes conditions que tous les autres champs de structure (décision LDM 1, décision LDM 2).

Par exemple (ces diagnostics sont silencieux par défaut) :

public struct S
{
    public S()
    {
        // CS9020 The 'this' object is read before all of its fields have been assigned, causing preceding implicit
        // assignments of 'default' to non-explicitly assigned fields.
        _ = P1;
    }

    public int P1 { get => field; }
}
public struct S
{
    public S()
    {
        // CS9020 The 'this' object is read before all of its fields have been assigned, causing preceding implicit
        // assignments of 'default' to non-explicitly assigned fields.
        P2 = 5;
    }

    public int P2 { get => field; set => field = value; }
}

Propriétés retournant des références

Comme avec les propriétés automatiques, le mot clé field ne pourra pas être utilisé dans les propriétés qui renvoient 'ref'. Les propriétés retournant des références ne peuvent pas avoir d'accesseurs set, et sans un accesseur set, l'accesseur get et l'initialisateur de la propriété seraient les seules choses capables d'accéder au champ de stockage. En l'absence de cas d'utilisation, ce n'est pas le moment de commencer à écrire des propriétés retournant des références en tant que propriétés automatiques.

Possibilité de valeurs nulles

Un principe de la fonctionnalité Types de référence nullables était de comprendre les habitudes de codage idiomatiques existantes en C# et d'exiger le moins de formalisme possible autour de ces modèles. La proposition de mot-clé field facilite l'utilisation de modèles simples et idiomatiques pour aborder des scénarios fréquemment demandés, tels que les propriétés initialisées paresseusement. Il est important que les types de référence nullable s'intègrent bien avec ces nouveaux modèles de codage.

Objectifs :

  • Un niveau raisonnable de sécurité null doit être assuré pour différents modèles d’utilisation de la fonctionnalité de mot clé field.

  • Les modèles qui utilisent le mot clé field doivent se sentir comme s’ils faisaient toujours partie de la langue. Évitez d'imposer des obstacles inutiles à l'utilisateur pour activer les Types de Référence Nullable dans le code qui est parfaitement idiomatique pour la fonctionnalité du mot-clé field.

L’un des principaux scénarios est les propriétés initialisées paresseusement :

public class C
{
    public C() { } // It would be undesirable to warn about 'Prop' being uninitialized here

    string Prop => field ??= GetPropValue();
}

Les règles de nullabilité suivantes s’appliquent non seulement aux propriétés qui utilisent le mot clé field, mais également aux propriétés automatiques existantes.

Caractère nul du champ de stockage

Consultez glossaire pour connaître les définitions de nouveaux termes.

Le champ de stockage a le même type que la propriété. Cependant, son annotation nullable peut être différente de celle de la propriété. Pour déterminer cette annotation nullable, nous introduisons le concept de null-resilience. Null-resilience signifie intuitivement que l'accesseur get de la propriété préserve la sécurité de valeur null même lorsque le champ contient la valeur default pour son type.

On détermine si une propriété stockée dans un champ est null-resilient ou non en effectuant une analyse nullable spéciale de son accesseur get.

  • Pour les besoins de cette analyse, field est temporairement supposé avoir une nullabilité annotée, par exemple, string?. Cela fait que field a un état initial maybe-null ou maybe-default dans l'accesseur get, en fonction de son type.
  • Ensuite, si l'analyse de la nullabilité de l'accesseur ne produit aucun avertissement de nullabilité, la propriété est null-resilient. Dans le cas contraire, elle n'est pas null-resilient.
  • Si la propriété n'a pas d'accesseur get, elle est (vacuously) null-resilient.
  • Si l’accesseur get est implémenté automatiquement, la propriété n’est pas résiliente à null.

La nullabilité du champ de stockage est déterminée comme suit :

  • Si le champ a des attributs nullabilité tels que [field: MaybeNull], AllowNull, NotNullou DisallowNull, l’annotation nullable du champ est identique à l’annotation nullable de la propriété.
    • Cela est dû au fait que lorsque l’utilisateur commence à appliquer des attributs de nullabilité au champ, nous ne voulons plus déduire quoi que ce soit, nous voulons simplement que la nullabilité soit ce que l’utilisateur a dit.
  • Si la propriété contenante a une nullabilité oubliée ou annotée, le champ de stockage a la même nullabilité que la propriété.
  • Si la propriété contenante a une nullabilité non annotée (par exemple string ou T) ou a l'attribut [NotNull], et que la propriété est null-resilient, alors le champ de stockage a une nullabilité annotée.
  • Si la propriété contenante a une nullabilité non annotée (par exemple string ou T) ou possède l'attribut [NotNull], et que la propriété n'est pas null-resilient, alors le champ de stockage a une nullabilité non annotée.

Analyse du constructeur

Actuellement, une propriété auto est traitée de manière très similaire à un champ ordinaire dans l'analyse des constructeurs nullables. Nous étendons ce traitement aux propriétés stockées dans un champ, en traitant chaque propriété stockée dans un champ comme un proxy de son champ de stockage.

Nous mettons à jour le langage de spécification suivant à partir de l’approche proposée précédente pour y parvenir :

À chaque « retour » explicite ou implicite dans un constructeur, nous donnons un avertissement pour chaque membre dont l’état de flux n’est pas compatible avec ses annotations et ses attributs de nullité. Si le membre est une propriété stockée dans un champ, l'annotation nullable du champ de stockage est utilisée pour cette vérification. Dans le cas contraire, c'est l'annotation nullable du membre lui-même qui est utilisée. Un proxy raisonnable est le suivant : si l'affectation du membre à lui-même au point de retour produit un avertissement de nullabilité, alors un avertissement de nullabilité sera produit au point de retour.

Notez qu’il s’agit essentiellement d’une analyse interprocedurale contrainte. Nous prévoyons que pour analyser un constructeur, il sera nécessaire d'effectuer une analyse de liaison et de « null-resilience » sur tous les accesseurs get applicables dans le même type, qui utilisent le mot-clé contextuel field et dont la nullabilité n'est pas annotée. Nous supposons que cela n'est pas excessivement coûteux parce que les corps des accesseurs ne sont généralement pas très complexes et que l'analyse de la « null-resilience » n'a besoin d'être effectuée qu'une seule fois, quel que soit le nombre de constructeurs dans le type.

Analyse des setters

Par souci de simplicité, nous utilisons les termes « setter » et « set accessor » pour faire référence à un accesseur set ou init.

Il est nécessaire de vérifier que les setters des propriétés stockées dans un champ initialisent effectivement le champ de stockage.

class C
{
    string Prop
    {
        get => field;

        // getter is not null-resilient, so `field` is not-annotated.
        // We should warn here that `field` may be null when exiting.
        set { }
    }

    public C()
    {
        Prop = "a"; // ok
    }

    public static void Main()
    {
        new C().Prop.ToString(); // NRE at runtime
    }
}

L’état de flux initial du champ de soutien dans le setter d’une propriété soutenue par un champ est déterminé comme suit :

  • Si la propriété a un initialiseur, l’état initial du flux est identique à l’état de flux de la propriété après avoir visité l’initialiseur.
  • Sinon, l’état initial du flux est identique à l’état de flux donné par field = default;.

À chaque « return » explicite ou implicite dans le setter, un avertissement est signalé si l'état de flux du champ de stockage est incompatible avec ses annotations et ses attributs de nullabilité.

Remarques

Cette formulation est intentionnellement très similaire à celle des champs ordinaires dans les constructeurs. Essentiellement, parce que seuls les accesseurs de propriété peuvent réellement faire référence au champ de stockage, le setter est traité comme un « mini-constructeur » pour le champ de stockage.

Comme avec les champs ordinaires, nous savons généralement que la propriété a été initialisée dans le constructeur, car elle a été définie, mais pas nécessairement. Le simple fait de revenir dans une branche où Prop != null était vrai est également suffisant pour notre analyse du constructeur, puisque nous comprenons que des mécanismes non suivis peuvent avoir été utilisés pour définir la propriété.

D'autres solutions ont été envisagées. Consulter la section Autres solutions de nullabilité.

nameof

Dans les endroits où field est un mot clé, nameof(field) ne parvient pas à compiler (décision LDM), comme nameof(nint). Ce n'est pas comme nameof(value), qui est la chose à utiliser lorsque les setters de propriété lancent des ArgumentException, comme certains le font dans les bibliothèques de base .NET. En revanche, nameof(field) n’a pas de cas d’usage attendus.

Remplacements

Le remplacement des propriétés peut utiliser field. Ces utilisations de field se réfèrent au champ de stockage de la propriété de remplacement, distinct de celui de la propriété de base si elle en a un. Il n'existe pas d'ABI permettant d'exposer le champ de stockage d'une propriété de base aux classes de remplacement, car cela romprait l'encapsulation.

Comme pour les propriétés auto, les propriétés qui utilisent le mot-clé field et qui remplacent une propriété de base doivent remplacer tous les accesseurs (décision du MLD).

Captures

field doit être capturée dans les fonctions locales et les lambdas, et les références aux field à partir des fonctions locales et des lambdas sont autorisées même s’il n’y a pas d’autres références (décision LDM 1, décision LDM 2) :

public class C
{
    public static int P
    {
        get
        {
            Func<int> f = static () => field;
            return f();
        }
    }
}

Avertissements sur l'utilisation des champs

Lorsque le mot clé field est utilisé dans un accesseur, l’analyse existante du compilateur des champs non attribués ou non lus inclut ce champ.

  • CS0414 : Le champ de stockage de la propriété « Xyz » est affecté, mais sa valeur n’est jamais utilisée
  • CS0649 : Le champ de stockage de la propriété « Xyz » n’est jamais affecté et aura toujours sa valeur par défaut

Modifications de spécification

Syntaxe

Lors de la compilation avec la version 14 ou ultérieure du langage, field est considéré comme un mot clé lorsqu’il est utilisé comme expression primaire (décision LDM) aux emplacements suivants (décision LDM) :

  • Dans le corps des méthodes des accesseurs get, set et init dans les propriétés mais pas dans les indexeurs
  • Dans les attributs appliqués à ces accesseurs
  • Dans les expressions lambda imbriquées et les fonctions locales, et dans les expressions LINQ dans ces accesseurs.

Dans tous les autres cas, y compris lors de la compilation avec la version 12 ou inférieure de la langue, field est considéré comme un identificateur.

primary_no_array_creation_expression
    : literal
+   | 'field'
    | interpolated_string_expression
    | ...
    ;

Propriétés

§15.7.1Propriétés - Général

Un property_initializer ne peut être donné que pour une propriété mise en œuvre automatiquement, et une propriété qui a un champ de stockage qui sera émis. Le property_initializer entraîne l'initialisation du champ sous-jacent de ces propriétés avec la valeur donnée par l'expression.

§15.7.4Implémentation automatique des propriétés

Une propriété mise en œuvre automatiquement (ou propriété automatique en abrégé) est une propriété non abstraite, non externe, non valorisée avec des corps d'accesseur à point-virgule uniquement. Les propriétés automatiques doivent avoir un accesseur get et peuvent éventuellement avoir un accesseur set.l'un ou l'autre ou les deux :

  1. un accesseur dont le corps est constitué d'un point-virgule uniquement
  2. l'utilisation du mot-clé contextuel field dans le corps de l'accesseur ou de l'expression de la propriété

Lorsqu'une propriété est spécifiée comme une propriété mise en œuvre automatiquement, un champ de stockage masqué sans nom est automatiquement disponible pour la propriété , et les accesseurs sont mis en œuvre pour lire et écrire dans ce champ de stockage. Pour les propriétés automatiques, tout accesseur get ne comportant qu'un point-virgule est implémenté setpour lire et tout accesseur ne comportant qu'un point-virgule est implémenté pour écrire dans son champ de stockage.

Le champ de stockage masqué n’est pas accessible, il peut être lu et écrit uniquement par le biais des accesseurs de propriété implémentés automatiquement, même dans le type conteneur.Le champ de stockage peut être référencé directement à l’aide du mot clé fielddans tous les accesseurs et dans le corps de l’expression de propriété. Étant donné que le champ n’est pas nommé, il ne peut pas être utilisé dans une expressionnameof.

Si la propriété auto n'a pas d'accesseur set, mais seulement un accesseur get avec point-virgule, le champ de stockage est considéré comme readonly (§15.5.3). Tout comme un champ readonly, une propriété auto en lecture seule (sans accesseur set ni accesseur init) peut également être assignée dans le corps du constructeur de la classe englobante. Une telle affectation affecte directement le champ de stockage en lecture seule de la propriété.

Une propriété auto n'est pas autorisée à avoir un seul accesseur set avec point-virgule uniquement sans un accesseur get.

Une propriété auto peut aussi avoir un property_initializer, qui est appliqué directement au champ de stockage comme un variable_initializer (§17.7).

L’exemple suivant :

// No 'field' symbol in scope.
public class Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

équivaut à la déclaration suivante :

// No 'field' symbol in scope.
public class Point
{
    public int X { get { return field; } set { field = value; } }
    public int Y { get { return field; } set { field = value; } }
}

qui équivaut à :

// No 'field' symbol in scope.
public class Point
{
    private int __x;
    private int __y;
    public int X { get { return __x; } set { __x = value; } }
    public int Y { get { return __y; } set { __y = value; } }
}

L’exemple suivant :

// No 'field' symbol in scope.
public class LazyInit
{
    public string Value => field ??= ComputeValue();
    private static string ComputeValue() { /*...*/ }
}

équivaut à la déclaration suivante :

// No 'field' symbol in scope.
public class Point
{
    private string __value;
    public string Value { get { return __value ??= ComputeValue(); } }
    private static string ComputeValue() { /*...*/ }
}

Alternatives

Autres solutions de nullabilité

En plus de l'approche de la null-resilience décrite dans la section sur la nullabilité, le groupe de travail a suggéré les alternatives suivantes à l'attention du MLD :

Ne rien faire

Nous n’avons pu introduire aucun comportement spécial ici. En vigueur :

  • Traiter une propriété stockée dans un champ de la même manière que les propriétés auto sont traitées aujourd'hui, doit être initialisée dans le constructeur sauf lorsqu'elle est marquée comme requise, etc.
  • Pas de traitement spécial de la variable de champ lors de l'analyse des accesseurs de propriété. Il s’agit simplement d’une variable avec le même type et la même nullabilité que la propriété.

Notez que cela entraînerait des avertissements gênants pour les scénarios de « propriété tardive », auquel cas les utilisateurs devraient probablement assigner null! ou quelque chose de similaire pour faire taire les avertissements du constructeur.
Une « sous-alternative » que nous pouvons envisager consiste également à ignorer complètement les propriétés à l'aide du mot-clé field pour l’analyse des constructeurs nullables. Dans ce cas, il n’y aurait aucun avertissement n’importe où au sujet de l’utilisateur qui a besoin d’initialiser quoi que ce soit, mais également aucune nuisance pour l’utilisateur, quel que soit le modèle d’initialisation qu’il peut utiliser.

Étant donné que nous prévoyons uniquement d’expédier la fonctionnalité de mot clé field sous la version préliminaire LangVersion dans .NET 9, nous nous attendons à pouvoir modifier le comportement nullable de cette fonctionnalité dans .NET 10. Par conséquent, nous pourrions envisager d’adopter une solution « à moindre coût » comme celle-ci à court terme, et de grandir jusqu’à l’une des solutions les plus complexes à long terme.

Attributs de nullabilité ciblés field

Nous pourrions introduire les valeurs par défaut suivantes, ce qui permettrait d'atteindre un niveau raisonnable de sécurité en matière de nullabilité, sans impliquer la moindre analyse interprocédurale :

  1. La variable field a toujours la même annotation nullable que la propriété.
  2. Les attributs de nullabilité [field: MaybeNull, AllowNull], etc. peuvent être utilisés pour personnaliser la nullabilité du champ de stockage.
  3. les propriétés stockées dans un champ sont vérifiées pour l'initialisation dans les constructeurs sur la base de l'annotation et des attributs nullables du champ.
  4. les setters des propriétés stockées dans un champ vérifient l'initialisation de field de la même manière que les constructeurs.

Cela signifierait que le « scénario little-l lazy » ressemblerait plutôt à ceci :

class C
{
    public C() { } // no need to warn about initializing C.Prop, as the backing field is marked nullable using attributes.

    [field: AllowNull, MaybeNull]
    public string Prop => field ??= GetPropValue();
}

Une raison pour laquelle nous avons évité d'utiliser des attributs de nullabilité ici est que ceux que nous avons sont vraiment orientés autour de la description des entrées et des sorties des signatures. Ils sont fastidieux à utiliser pour décrire la nullabilité des variables de longue durée.

  • Dans la pratique, [field: MaybeNull, AllowNull] est nécessaire pour que le champ se comporte « raisonnablement » comme une variable nullable, qui donne peut-être un état de flux initial null et permet l’écriture de valeurs Null possibles. Cela semble ennuyeux de demander aux utilisateurs de le faire pour des scénarios « little-l lazy » relativement courants.
  • Si nous avons poursuivi cette approche, nous envisageons d’ajouter un avertissement lorsque [field: AllowNull] est utilisé, suggérant également d’ajouter MaybeNull. Cela est dû au fait que AllowNull par lui-même ne fait pas ce dont les utilisateurs ont besoin en dehors d’une variable nullable : il suppose que le champ n’est initialement pas null quand nous n’avons jamais vu quoi que ce soit d’écriture.
  • Nous pourrions également envisager d’ajuster le comportement de [field: MaybeNull] sur le mot clé field, ou même les champs en général, pour autoriser l’écriture de valeurs Null dans la variable, comme si AllowNull étaient également présents implicitement.

Réponses aux questions LDM

Emplacements de syntaxe pour les mots clés

Dans les accesseurs où field et value pourraient se lier à un champ de stockage synthétisé ou à un paramètre setter implicite, dans quels emplacements de syntaxe les identificateurs doivent-ils être considérés comme des mots clés ?

  1. toujours
  2. expressions primaires uniquement
  3. jamais

Les deux premiers cas constituent des changements cassants.

Si les identificateurs sont toujours considérés comme des mots clés, c’est une modification cassante comme dans l'exemple suivant :

class MyClass
{
    private int field;
    public int P => this.field; // error: expected identifier

    private int value;
    public int Q
    {
        set { this.value = value; } // error: expected identifier
    }
}

Si les identificateurs sont des mots-clés lorsqu'ils sont utilisés comme expressions primaires uniquement, le changement cassant est moindre. La rupture la plus courante peut être l'utilisation non qualifiée d'un membre existant nommé field.

class MyClass
{
    private int field;
    public int P => field; // binds to synthesized backing field rather than 'this.field'
}

Il y a aussi une interruption lorsque field ou value est redéclaré dans une fonction imbriquée. Il peut s'agir de la seule rupture pour value dans les expressions primaires.

class MyClass
{
    private IEnumerable<string> _fields;
    public bool HasNotNullField
    {
        get => _fields.Any(field => field is { }); // 'field' binds to synthesized backing field
    }
    public IEnumerable<string> Fields
    {
        get { return _fields; }
        set { _fields = value.Where(value => Filter(value)); } // 'value' binds to setter parameter
    }
}

Si les identificateurs ne sont jamais considérés comme des mots-clés, ils ne se lieront qu'à un champ de stockage synthétisé ou au paramètre implicite lorsque les identificateurs ne se lient pas à d'autres membres. Il n'y a pas d'un changement cassant dans ce cas.

Répondre

est un mot clé dans les accesseurs appropriés lorsqu’ils sont utilisés en tant qu’expression primaire uniquement ; n’est jamais considéré comme un mot clé.

Scénarios similaires à { set; }

{ set; } est actuellement interdit et cela est logique : le champ que cela crée ne peut jamais être lu. Il existe désormais de nouvelles façons de se retrouver dans une situation où le setter introduit un champ de stockage qui n'est jamais lu, comme l'expansion de { set; } en { set => field = value; }.

Parmi ces scénarios, lequel doit être autorisé à compiler ? Supposons que l’avertissement « champ n’est jamais lu » s’applique comme avec un champ déclaré manuellement.

  1. { set; } - Non autorisé aujourd’hui, continuez à interdire
  2. { set => field = value; }
  3. { get => unrelated; set => field = value; }
  4. { get => unrelated; set; }
  5. {
        set
        {
            if (field == value) return;
            field = value;
            SendEvent(nameof(Prop), value);
        }
    }
    
  6. {
        get => unrelated;
        set
        {
            if (field == value) return;
            field = value;
            SendEvent(nameof(Prop), value);
        }
    }
    

Répondre

N'interdit que ce qui est déjà interdit aujourd'hui dans les propriétés automatiques, le set; sans corps.

field dans l'accesseur à l'événement

field doit-il être un mot-clé dans un accesseur d’événements, et le compilateur doit-il générer un champ de stockage ?

class MyClass
{
    public event EventHandler E
    {
        add { field += value; }
        remove { field -= value; }
    }
}

Recommandation : field n'est pas un mot clé dans un accesseur d'événement, et aucun champ de stockage n'est généré.

Répondre

Recommandation prise. field n'est pas un mot-clé dans un accesseur d'événement et aucun champ de stockage n'est généré.

Nullité de field

La possibilité de nullabilité proposée de field doit-elle être acceptée ? Voir la section sur la nullabilité et la question ouverte qui s'y rapporte.

Répondre

La proposition générale est adoptée. Un comportement spécifique a encore besoin d’une révision supplémentaire.

field dans un initialisateur de propriété

field doit-il être un mot-clé dans un initialisateur de propriété et se lier au champ de stockage ?

class A
{
    const int field = -1;

    object P1 { get; } = field; // bind to const (ok) or backing field (error)?
}

Existe-t-il des scénarios utiles pour référencer le champ de stockage dans l'initialisateur ?

class B
{
    object P2 { get; } = (field = 2);        // error: initializer cannot reference instance member
    static object P3 { get; } = (field = 3); // ok, but useful?
}

Dans l’exemple ci-dessus, la liaison au champ de stockage doit entraîner une erreur : « l’initialiseur ne peut pas référencer un champ non statique ».

Répondre

Nous allons lier l’initialiseur comme dans les versions précédentes de C#. Nous ne mettrons pas le champ de stockage dans l'étendue, et nous n'empêcherons pas non plus la référence à d'autres membres nommés field.

Interaction avec les propriétés partielles

Initialiseurs

Lorsqu’une propriété partielle utilise field, quelles parties doivent être autorisées à avoir un initialiseur ?

partial class C
{
    public partial int Prop { get; set; } = 1;
    public partial int Prop { get => field; set => field = value; } = 2;
}
  • Il semble évident qu’une erreur doit se produire lorsque les deux parties ont un initialiseur.
  • Nous pouvons considérer les cas d’usage où la définition ou la partie d’implémentation pourrait vouloir définir la valeur initiale de la field.
  • Il semble que si nous autorisez l’initialiseur sur la partie définition, il force effectivement l’implémenteur à utiliser field afin que le programme soit valide. C’est bien ?
  • Nous pensons qu’il sera courant que les générateurs utilisent field chaque fois qu’un champ de stockage du même type est nécessaire dans l’implémentation. Cela est en partie dû au fait que les générateurs souhaitent souvent permettre à leurs utilisateurs d'utiliser des attributs ciblés [field: ...] dans la partie concernant la définition de la propriété. L’utilisation du mot clé field épargne à l’implémenteur du générateur le problème du « transfert » de tels attributs dans un champ généré et neutralise les avertissements concernant la propriété. Ces mêmes générateurs sont susceptibles d’autoriser l’utilisateur à spécifier une valeur initiale pour le champ.

Recommandation : autorisez un initialisateur sur l'une ou l'autre partie d'une propriété partielle lorsque la partie d'implémentation utilise field. Signalez une erreur si les deux parties ont un initialiseur.

Répondre

Recommandation acceptée. La déclaration ou l'implémentation des propriétés peut utiliser un initialisateur, mais pas les deux en même temps.

Auto-accesseurs

Telle qu'elle a été conçue à l'origine, l'implémentation des propriétés partielles doit avoir des corps pour tous les accesseurs. Toutefois, les itérations récentes de la fonctionnalité du mot-clé field ont inclus la notion d'« auto-accesseurs ». Les implémentations de propriétés partielles doivent-elles être en mesure d’utiliser ces accesseurs ? S'ils sont utilisés exclusivement, cela sera indiscernable d'une déclaration définissante.

partial class C
{
    public partial int Prop0 { get; set; }
    public partial int Prop0 { get => field; set => field = value; } // this is equivalent to the two "semi-auto" forms below.

    public partial int Prop1 { get; set; }
    public partial int Prop1 { get => field; set; } // is this a valid implementation part?

    public partial int Prop2 { get; set; }
    public partial int Prop2 { get; set => field = value; } // what about this? will there be disagreement about which is the "best" style?

    public partial int Prop3 { get; }
    public partial int Prop3 { get => field; } // it will only be valid to use at most 1 auto-accessor, when a second accessor is manually implemented.

Recommandation : interdisez les auto-accesseurs dans les implémentations de propriétés partielles, car les limitations concernant le moment où ils seraient utilisables sont plus déroutantes à suivre que l'avantage qu'il y a à les autoriser.

Répondre

Au moins un accesseur implémentant doit être implémenté manuellement, mais l’autre accesseur peut être implémenté automatiquement.

Champ en lecture seule

Quand le champ de stockage synthétisé doit-il être considéré comme étant en lecture seule ?

struct S
{
    readonly object P0 { get => field; } = "";         // ok
    object P1          { get => field ??= ""; }        // ok
    readonly object P2 { get => field ??= ""; }        // error: 'field' is readonly
    readonly object P3 { get; set { _ = field; } }     // ok
    readonly object P4 { get; set { field = value; } } // error: 'field' is readonly
}

Lorsque le champ de stockage est considéré comme étant en lecture seule, le champ émis dans les métadonnées est marqué initonly, et une erreur est signalée si field est modifié autrement que dans un initialisateur ou un constructeur.

Recommandation : le champ de stockage synthétisé est en lecture seule lorsque le type contenant est un struct et que la propriété ou le type contenant est déclaré readonly.

Répondre

La recommandation est acceptée.

Contexte en lecture seule et set

Un accesseur set doit-il être autorisé dans un contexte readonly pour une propriété qui utilise field?

readonly struct S1
{
    readonly object _p1;
    object P1 { get => _p1; set { } }   // ok
    object P2 { get; set; }             // error: auto-prop in readonly struct must be readonly
    object P3 { get => field; set { } } // ok?
}

struct S2
{
    readonly object _p1;
    readonly object P1 { get => _p1; set { } }   // ok
    readonly object P2 { get; set; }             // error: auto-prop with set marked readonly
    readonly object P3 { get => field; set { } } // ok?
}

Répondre

Il peut y avoir des scénarios dans lesquels vous implémentez un accesseur set sur une structure readonly et vous le transmettez ou lancez. Nous allons l'autoriser.

Code [Conditional]

Le champ synthétisé doit-il être généré lorsque field est utilisé uniquement dans les appels omis des méthodes conditionnelles ?

Par exemple, un champ de stockage doit-il être généré pour les éléments suivants dans une build non-DEBUG ?

class C
{
    object P
    {
        get
        {
            Debug.Assert(field is null);
            return null;
        }
    }
}

Pour référence, les champs pour les paramètres du constructeur primaire sont générés dans des cas similaires. Voir sharplab.io.

Recommandation : le champ de stockage est généré lorsque field n'est utilisé que dans des appels omis des méthodes conditionnelles.

Répondre

Conditional code peut avoir des effets sur du code non conditionnel, par exemple Debug.Assert qui modifie la nullabilité. Il serait étrange si field n’avait pas d’impact similaire. Il est également peu probable qu’il arrive dans la plupart du code, donc nous allons faire la chose simple et accepter la recommandation.

Propriétés de l’interface et accesseurs automatiques

Une combinaison d’accesseurs implémentés manuellement et automatiquement est-elle reconnue pour une propriété interface où l’accesseur implémenté automatiquement fait référence à un champ de stockage synthétisé ?

Pour une propriété d'instance, une erreur sera signalée indiquant que les champs d'instance ne sont pas pris en charge.

interface I
{
           object P1 { get; set; }                           // ok: not an implementation
           object P2 { get => field; set { field = value; }} // error: instance field

           object P3 { get; set { } } // error: instance field
    static object P4 { get; set { } } // ok: equivalent to { get => field; set { } }
}

Recommandation : les auto-accesseurs sont reconnus dans les propriétés interface, et les auto-accesseurs font référence à un champ de stockage synthétisé. Pour une propriété d’instance, il est signalé qu’une erreur indique que les champs d’instance ne sont pas pris en charge.

Répondre

La normalisation autour du champ d’instance lui-même étant la cause de l’erreur est cohérente avec les propriétés partielles dans les classes, et nous aimons ce résultat. La recommandation est acceptée.