Structs par défaut automatiques
Remarque
Cet article est une spécification de fonctionnalité. La spécification sert de document de conception pour la fonctionnalité. Elle inclut les changements de spécification proposés, ainsi que les informations nécessaires à la conception et au développement de la fonctionnalité. Ces articles sont publiés jusqu'à ce que les changements proposés soient finalisés et incorporés dans la spécification ECMA actuelle.
Il peut y avoir des différences entre la spécification de la fonctionnalité et l'implémentation réalisée. Ces différences sont consignées dans les notes pertinentes de la réunion de conception linguistique (LDM).
Pour en savoir plus sur le processus d'adoption des speclets de fonctionnalité dans la norme du langage C#, consultez l'article sur les spécifications.
Problème de champion : https://github.com/dotnet/csharplang/issues/5737
Récapitulatif
Cette fonctionnalité permet que, dans les constructeurs de structures, nous identifiions les champs qui n'ont pas été explicitement assignés par l'utilisateur avant le retour ou l'utilisation, et de les initialiser implicitement à default
au lieu de produire des erreurs d'affectation définitives.
Motivation
Cette proposition est soulevée comme une atténuation possible des problèmes d'utilisation trouvés dans dotnet/csharplang#5552 et dotnet/csharplang#5635, ainsi que pour répondre à #5563 (tous les champs doivent être définitivement assignés, mais field
n'est pas accessible à l'intérieur du constructeur).
Depuis C# 1.0, les constructeurs de structs doivent affecter définitivement this
comme s’il s’agissait d’un paramètre out
.
public struct S
{
public int x, y;
public S() // error: Fields 'S.x' and 'S.y' must be fully assigned before control is returned to the caller
{
}
}
Cela pose des problèmes lorsque des setters sont définis manuellement sur des propriétés semi-automatiques, puisque le compilateur ne peut pas considérer l'affectation de la propriété comme équivalente à l'affectation du champ d'appui.
public struct S
{
public int X { get => field; set => field = value; }
public S() // error: struct fields aren't fully assigned. But caller can only assign 'this.field' by assigning 'this'.
{
}
}
Nous partons du principe que l’introduction de restrictions plus précises pour les setters, comme un schéma où le setter ne prend pas ref this
, mais prend plutôt out field
en tant que paramètre, est trop exclusif et incomplet pour certains cas d’usage.
Une tension fondamentale que nous rencontrons est que, lorsque les propriétés de structure ont des setters implémentés manuellement, les utilisateurs doivent souvent effectuer une sorte de « répétition » en répétant soit des affectations, soit leur logique.
struct S
{
private int _x;
public int X
{
get => _x;
set => _x = value >= 0 ? value : throw new ArgumentOutOfRangeException();
}
// Solution 1: assign some value in the constructor before "really" assigning through the property setter.
public S(int x)
{
_x = default;
X = x;
}
// Solution 2: assign the field once in the constructor, repeating the implementation of the setter.
public S(int x)
{
_x = x >= 0 ? x : throw new ArgumentOutOfRangeException();
}
}
Discussion précédente
Un petit groupe a examiné ce problème et a envisagé quelques solutions possibles :
- Exiger que les utilisateurs attribuent
this = default
lorsque les setters des propriétés semi-automatiques sont implémentés manuellement. Nous nous accordons à dire qu’il s’agit de la mauvaise solution, car elle élimine les valeurs définies dans les initialiseurs de champ. - Initialiser implicitement tous les champs d'appui des propriétés auto/semi-auto.
- Cela résout le problème des "déclarateurs de propriétés semi-automatiques" et place clairement les champs explicitement déclarés sous des règles différentes : « ne pas initialiser implicitement mes champs, mais initialiser implicitement mes propriétés automatiques ».
- Fournissez un moyen d'attribuer le champ de stockage d'une propriété semi-automatisée et enjoignez aux utilisateurs de l'attribuer.
- Cela pourrait être fastidieux par rapport à (2). Une propriété automatique est censée être « automatique », et cela peut inclure l’initialisation « automatique » du champ. Cela pourrait introduire une confusion quant au moment où le champ sous-jacent est assigné par une assignation à la propriété, et au moment où l'outil de définition de la propriété est appelé.
Nous avons également reçu des commentaires d'utilisateurs qui souhaitent, par exemple, inclure quelques initialisateurs de champs dans des structures sans avoir à tout affecter explicitement. Nous pouvons résoudre ce problème ainsi que le problème de « propriété semi-automatique avec setter implémenté manuellement » en même temps.
struct MagnitudeVector3d
{
double X, Y, Z;
double Magnitude = 1;
public MagnitudeVector3d() // error: must assign 'X', 'Y', 'Z' before returning
{
}
}
Ajustement de l’affectation définitive
Au lieu d’effectuer une analyse des affectation définitives afin de donner des erreurs pour les champs non attribués sur this
, notre objectif est de déterminer quels champs doivent être initialisés implicitement. Cette initialisation est insérée au début du constructeur.
struct S
{
int x, y;
// Example 1
public S()
{
// ok. Compiler inserts an assignment of `this = default`.
}
// Example 2
public S()
{
// ok. Compiler inserts an assignment of `y = default`.
x = 1;
}
// Example 3
public S()
{
// valid since C# 1.0. Compiler inserts no implicit assignments.
x = 1;
y = 2;
}
// Example 4
public S(bool b)
{
// ok. Compiler inserts assignment of `this = default`.
if (b)
x = 1;
else
y = 2;
}
// Example 5
void M() { }
public S(bool b)
{
// ok. Compiler inserts assignment of `y = default`.
x = 1;
if (b)
M();
y = 2;
}
}
Dans les exemples (4) et (5), le codegen généré a parfois des « affectations doubles » de champs. Cela ne pose généralement pas de problème, mais pour les utilisateurs qui s'inquiètent de ces doubles affectations, nous pouvons émettre ce qui était auparavant des diagnostics d'erreur d'affectation définitive comme des diagnostics d'avertissement disabled-by-default.
struct S
{
int x;
public S() // warning: 'S.x' is implicitly initialized to 'default'.
{
}
}
Les utilisateurs qui définissent la gravité de ce diagnostic comme étant une « erreur » optent pour le comportement antérieur à C# 11. Ces utilisateurs sont pour ainsi dire "exclus" des propriétés semi-automatiques lorsque les setters sont implémentés manuellement.
struct S
{
public int X
{
get => field;
set => field = field < value ? value : field;
}
public S() // error: backing field of 'S.X' is implicitly initialized to 'default'.
{
X = 1;
}
}
À première vue, cela ressemble à un « trou » dans la fonctionnalité, mais c'est en fait la bonne chose à faire. En activant le diagnostic, l’utilisateur nous indique qu’il ne veut pas que le compilateur initialise implicitement ses champs dans le constructeur. Il n’existe ici aucun moyen d’éviter l’initialisation implicite. Par conséquent, la solution consiste à utiliser un autre moyen d’initialiser le champ qu’un setter implémenté manuellement, par exemple la déclaration manuelle du champ et son affectation, ou l’inclusion d’un initialiseur de champ.
Actuellement, le JIT n'élimine pas les magasins morts par le biais des refs, ce qui signifie que ces initialisations implicites ont un coût réel. Mais il devrait être possible de résoudre ce problème. https://github.com/dotnet/runtime/issues/13727
Il est important de noter que l’initialisation de champs individuels au lieu de l’ensemble de l’instance n’est qu’une optimisation. Le compilateur devrait probablement être libre d'implémenter l'heuristique qu'il souhaite, tant qu'il respecte l'invariant selon lequel les champs qui ne sont pas définitivement assignés à tous les points de retour ou avant tout accès de this
à un membre qui n'est pas un champ sont implicitement initialisés.
Par exemple, si un struct a 100 champs et qu’un seul champ est explicitement initialisé, il peut être plus judicieux d’effectuer une opération initobj
sur la totalité des champs, au lieu d’émettre implicitement une opération initobj
pour les 99 autres champs. Toutefois, une implémentation qui émet implicitement initobj
pour les 99 autres champs serait toujours valide.
Modifications apportées à la spécification du langage
Nous adapterons la section suivante de la norme :
https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12814-this-access
Si la déclaration du constructeur n’a pas d’initialiseur de constructeur, la variable
this
se comporte exactement comme un paramètreout
du type struct. En particulier, cela signifie que la variable doit être assurément affectée dans chaque chemin d’exécution du constructeur d’instance.
Nous ajusterons ce langage pour qu’il dise ce qui suit :
Si la déclaration du constructeur n’a pas d’initialiseur de constructeur, la variable this
se comporte de la même façon qu’un paramètre out
du type struct, sauf qu’il ne s’agit pas d’une erreur lorsque les exigences des affectations définitives (§9.4.1) ne sont pas remplies. Au lieu de cela, nous introduisons les comportements suivants :
- Lorsque la variable
this
elle-même ne répond pas aux exigences, toutes les variables d’instance non affectées dansthis
à tous les points où les exigences sont violées sont implicitement initialisées pour représenter la valeur par défaut (§9.3) dans une phase d’initialisation avant l’exécution d’un autre code dans le constructeur. - Lorsqu’une variable d’instance v dans
this
ne répond pas aux exigences, ou qu’une variable d’instance à un niveau quelconque d’imbrication dans v ne répond pas aux exigences, v est implicitement initialisé pour représenter la valeur par défaut dans une phase d’initialisation avant l’exécution d’un autre code dans le constructeur.
Concevoir des réunions
C# feature specifications