Partager via


mot clé field dans les propriétés

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

Récapitulatif

É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. Il est parfois 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 la déclaration d'un champ d'appui. 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 s'agit d'appliquer une contrainte pour garantir la validité d'une valeur, ou de détecter et de propager des mises à jour, par exemple en déclenchant 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 : Abréviation de « accesseur implémenté automatiquement ». Il s'agit d'un accesseur qui n'a pas de corps. L’implémentation et la mémoire de stockage sont fournies 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 ayant un accesseur init, tout ce qui s'applique ci-dessous à set s'applique à l'accesseur init.

Il y a deux changements 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 une propriété automatique.

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 Set-only 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 sur un 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).

Appeler un setter pour un initialisateur n'est pas une option ; les initialisateurs sont traités avant d'appeler les constructeurs de base, et il est illégal d'appeler une méthode d'instance avant que le constructeur de base ne soit appelé. Ceci est également important pour l'initialisation par défaut/l'affectation définie des structures.

Cela permet un contrôle flexible de l'initialisation. Si vous voulez initialiser sans appeler le setter, vous utilisez un initialisateur 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'utilité de cette méthode. Nous pensons que le mot-clé field sera largement utilisé avec les modèles de vues en raison de la solution élégante qu'il apporte au 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 à 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 devrait être assuré pour les différents modèles d'utilisation de la fonctionnalité du mot-clé field.

  • Les modèles qui utilisent le mot-clé field doivent donner l'impression d'avoir toujours fait partie du langage. É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 nullité suivantes s'appliquent non seulement aux propriétés qui utilisent le mot-clé field, mais aussi aux propriétés automatiques existantes.

Caractère nul du champ de stockage

Voir le glossaire pour la définition des 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 auto-implémenté, la propriété n'est pas résiliente à la nullité.

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

  • Si le champ possède des attributs de nullité tels que [field: MaybeNull], AllowNull, NotNull ou DisallowNull, l'annotation nullable du champ est la même que 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 des constructeurs

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.

Pour ce faire, nous mettons à jour le langage de spécification suivant à partir de l'approche proposée précédemment :

À 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 interprocédurale 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 désigner un accessor 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 initialisateur, alors l'état initial du flux est le même que l'état du flux de la propriété après avoir visité l'initialisateur.
  • Sinon, l'état de flux initial est le même que 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 pour les champs ordinaires, nous savons généralement que la propriété a été initialisée dans le constructeur parce qu'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 cas où field est un mot-clé, nameof(field) échouera à la compilation (décision du MLD), 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 aucun cas d'utilisation prévu.

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 pouvoir être capturé dans des fonctions locales et des lambdas, et les références à field à l'intérieur de fonctions locales et de 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 sur les champs non assignés ou non lus inclura ce champ.

  • CS0414 : Le champ d'appui de la propriété 'Xyz' est assigné mais sa valeur n'est jamais utilisée.
  • CS0649 : Le champ d'appui de la propriété 'Xyz' n'est jamais assigné et aura toujours sa valeur par défaut.

Modifications des spécifications

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 du langage, field est considéré comme un identifiant.

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 d'appui caché est inaccessible, il ne peut être lu et écrit qu'à travers les accesseurs de propriété automatiquement implémentés, même au sein du type contenant. Le champ d'appui peut être référencé directement en utilisant le mot-clé field dans tous les accesseurs et dans le corps de l'expression de la propriété. Comme le champ n'est pas nommé, il ne peut pas être utilisé dans une nameofexpression.

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

est équivalent à 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; } }
}

ce qui est équivalent à :

// 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() { /*...*/ }
}

est équivalent à 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 pourrions n'introduire aucun comportement spécial ici. En effet :

  • 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 ayant le même type et la même nullité 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 concernant la nécessité pour l'utilisateur d'initialiser quoi que ce soit, mais également aucune nuisance pour l'utilisateur, quel que soit le modèle d'initialisation qu'il utilise.

É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 « moins coûteuse » comme celle-ci à court terme, et d'évoluer vers l'une des solutions 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. Ces annotations sont lourdes à utiliser pour décrire la nullité des variables à longue durée de vie.

  • Dans la pratique, [field: MaybeNull, AllowNull] est nécessaire pour que le champ se comporte « raisonnablement » comme une variable nullable, qui donne un état de flux initial peut-être nul et permet d'y écrire des valeurs nulles possibles. Cela semble ennuyeux de demander aux utilisateurs de le faire pour des scénarios « little-l lazy » relativement courants.
  • Si nous poursuivions cette approche, nous envisagerions d'ajouter un avertissement lorsque [field: AllowNull] est utilisé, en suggérant d'ajouter également MaybeNull. En effet, AllowNull en lui-même ne fait pas ce que les utilisateurs attendent d'une variable nullable : il suppose que le champ est initialement non nul alors que rien n'a encore été écrit dans ce champ.
  • On pourrait aussi envisager d'ajuster le comportement de [field: MaybeNull] sur le mot-clé field, ou même sur les champs en général, pour permettre d'écrire aussi des null dans la variable, comme si AllowNull était implicitement aussi présent.

Réponses aux questions sur le MLD

Emplacements syntaxiques des mots-clés

Dans les accesseurs où field et value pourraient se lier à un champ d'appui synthétisé ou à un paramètre setter implicite, dans quels emplacements syntaxiques les identifiants 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éponse

field est un mot-clé dans les accesseurs appropriés lorsqu'il est utilisé comme expression primaire uniquement ; value n'est jamais considéré comme un mot-clé.

Les scénarios similaires à { set; }

{ set; } est actuellement interdit, ce qui est logique : le champ ainsi créé 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; }.

Lequel de ces scénarios devrait être autorisé à la compilation ? Supposez que l'avertissement « le champ n'est jamais lu » s'applique de la même manière qu'avec un champ déclaré manuellement.

  1. { set; } - Interdit aujourd'hui, continuer à 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éponse

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éponse

Recommandation adoptée. 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 proposition de nullité de field doit-elle être acceptée ? Voir la section sur la nullabilité et la question ouverte qui s'y rapporte.

Réponse

La proposition générale est adoptée. Le comportement spécifique doit encore être examiné.

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 avec le champ d'appui devrait entraîner une erreur : « initializer cannot reference non-static field ».

Réponse

Nous lierons l'initialisateur 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 initialisateur ?

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 devrait se produire lorsque les deux parties ont un initialisateur.
  • 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 autorisons l'initialisateur dans la partie définition, cela oblige effectivement l'exécutant à utiliser field pour que le programme soit valide. Cela vous convient-il ?
  • Nous pensons qu'il sera courant pour les générateurs d'utiliser field chaque fois qu'un champ d'appui du même type sera 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 voudront probablement aussi permettre à l'utilisateur de 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 initialisateur.

Réponse

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 devraient-elles pouvoir utiliser de tels 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éponse

Au moins un accesseur d'implémentation 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éponse

La recommandation est acceptée.

Contexte en lecture seule et set

Un accesseur set devrait-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éponse

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 d'appui devrait-il être généré pour ce qui suit dans une version 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éponse

Conditional code peut avoir des effets sur du code non conditionnel, par exemple Debug.Assert qui modifie la nullabilité. Il serait étrange que field n'ait pas d'impact similaire. Il est également peu probable que cela se produise dans la plupart des codes, donc nous ferons la chose la plus simple et accepterons la recommandation.

Propriétés d'interface et auto-accesseurs

Une combinaison d'accesseurs implémentés manuellement et automatiquement est-elle reconnue pour une propriété interface lorsque l'accesseur implémenté automatiquement fait référence à un champ d'appui 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éponse

La standardisation autour du champ d'instance lui-même comme 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.