Dec 22, 2023

Les structures en C# sont amusantes - Partie 5/9: Des autres scénarios dans lesquels le comportement des constructeurs de structure peut vous surprendre

Leia este post em português

Read this post in English.

  1. Les structures en C# sont amusantes
  2. Brève introduction aux Value Types vs Reference Types.
  3. Initialisation des champs dans les structures.
  4. Comportement des constructeurs dans structures.
  5. Des autres scénarios dans lesquels le comportement des constructeurs de structure peut vous surprendre (cet post).
  6. Struct avec des valeurs d'argument par défaut dans les constructeurs, ou, n'êtes-vous pas encore confus ?
  7. Le modificateur `required`de C # 11 ne sauvegardera pas votre c*l emploi.
  8. Structure utilisée comme valeurs d'argument défaut.
  9. Bonus: L'evolution des structures en C#

Dans l'article précédent, j'ai présenté un scénario dans lequel un struct ctor ne serait pas invoqué, peu importe la façon dont la syntaxe peut nous laisser croire qu'il le serait. Cet article développe cela en ajoutant quelques autres scénarios présentés dans le code ci-dessous:

Compte tenu du code ci-dessus, qu'attendez-vous d'être imprimé ? 42, 42, -1, 42?

Avec la connaissance du post précédent, nous savons que le quatrième appel (marqué comme //4) invoquera le constructeur sans paramètre d'où s_4.v aura une valeur de 42 ; mais qu'en est-il des 3 autres cas

Si nous inspectons l'IL généré, nous verrons que le compilateur a émis un appel au constructeur S(int) pour s_3 mais qu'aucun constructeur n'a été invoqué pour les deux premiers cas, ce qui signifie que s_1 et s_2 auront leur champ v initialisé à 0, donc le le code ci-dessus s'imprimera 0, 0, -1, 42.

Mais, porquoi ?

On pourrait dire que dans les 4 scénarios, un type valeur est instancié et à partir duquel un constructeur aurait dû être exécuté. Examinons donc un peu plus les 2 scénarios ci-dessus pour lesquels aucun constructeur n'est invoqué et essayons de raisonner sur la motivation derrière cette décision.

En commençant par s_1, si nous regardons la documentation de l'expression par défaut, nous pouvons lire:

Une expression de valeur par défaut est utilisée pour obtenir la valeur par défaut (valeurs par défaut) d’un type.

qui  référence de section valeurs par défaut où nous pouvons lire:

Pour une variable d’un Value_type, la valeur par défaut est la même que la valeur calculée par le constructeur par défaut de Value_type(constructeurs par défaut).

qui lui-même fait référence à la section Constructeurs par Défaut où l'on peut enfin lire:

Tous les types valeur déclarent implicitement un constructeur d’instance sans paramètre public appelé le constructeur par défaut. Le constructeur par défaut retourne une instance initialisée à zéro appelée valeur par défaut pour le type de valeur :
...
  • Pour un struct_type, la valeur par défaut est la valeur produite en affectant à tous les champs de type valeur leur valeur par défaut et à tous les champs de type référence la valeur null .

Ainsi, même si la documentation mentionne l'invocation du constructeur par défaut, elle indique également que le constructeur par défaut pour un type valeur équivaut à mettre à zéro toute la mémoire réservée au type1. Ainsi, aucun constructeur invoqué dans ce scénario n'est le comportement attendu.

Cela nous laisse avec notre dernier scénario : des tableaux (arrays) de types valeur. Pour résoudre ce problème, recourons à nouveau à ce que dit la spécification du langage à propos de la création des tableaux :

Les instances de tableau sont créées par array_creation_expression (expressions de création de tableau)
...
Les éléments des tableaux créés par array_creation_expression s sont toujours initialisés à leur valeur par défaut (valeurs par défaut).

ce qui signifie que lorsqu'un tableau est instancié, tous ses éléments sont définis sur la valeur par défaut du type tableau, ce qui pour les types valeur, comme nous l'avons vu ci-dessus, équivaut à remettre à zéro la mémoire allouée.

Je crois que ce comportement consistant à ne pas invoquer le constructeur par défaut sur les types valeur a deux motivations principales : i) historiquement, C# n'a pas pris en charge le concept de constructeurs explicites sans paramètre (par défaut) sur les types valeur jusqu'à C# 102, et ii) performance : imaginez du code faire quelque chose comme
new S[1_000_000]; si les constructeurs par défaut devaient être exécutés, cela pourrait prendre un temps imprévisible.

As always, all feedback is welcome.

No comments: