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;
etinit;
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;
ouinit;
).
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 :
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).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 quefield
a un état initial maybe-null ou maybe-default dans l'accesseurget
, 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
,NotNull
ouDisallowNull
, 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
ouT
) 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
ouT
) 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
etinit
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, etune 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 :
- un accesseur dont le corps est constitué d'un point-virgule uniquement
- 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 accesseurget
ne comportant qu'un point-virgule est implémentéset
pour 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éfield
dans 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é commereadonly
(§15.5.3). Tout comme un champreadonly
, 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 enlecture seulede la propriété.Une propriété auto n'est pas autorisée à avoir un seul accesseur
set
avec point-virgule uniquement sans un accesseurget
.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 :
- La variable
field
a toujours la même annotation nullable que la propriété. - Les attributs de nullabilité
[field: MaybeNull, AllowNull]
, etc. peuvent être utilisés pour personnaliser la nullabilité du champ de stockage. - 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.
- 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’ajouterMaybeNull
. 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 siAllowNull
é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 ?
- toujours
- expressions primaires uniquement
- 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
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.
{ set; }
- Non autorisé aujourd’hui, continuez à interdire{ set => field = value; }
{ get => unrelated; set => field = value; }
{ get => unrelated; set; }
-
{ set { if (field == value) return; field = value; SendEvent(nameof(Prop), value); } }
-
{ 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.
C# feature specifications