Nov 22, 2023

Les structures en C# sont amusantes - Partie 4/9: Comportement des constructeurs dans structures

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 (cet post).
  5. Des autres scénarios dans lesquels le comportement des constructeurs de structure peut vous surprendre.
  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 le post précédent de notre série sur les Value Types, nous avons présenté le code ci-dessous:

qui imprime 0 (par opposition à 32 comme certains pourraient s'y attendre) et a également exploré les bases de la gestion de l'initialisation des champs en C#. Cet article élargit cette discussion en explorant certains aspects clés qui contribuent à la disparité entre le comportement attendu/observé.

Commençons par la déclaration suivante du post précédent:

D'une manière trop simpliste, chaque fois que le compilateur C# trouve une initialisation de champ, il déplacera simplement le code d'initialisation vers les constructeurs, c'est-à-dire que l'initialisation d'un champ équivaut à définir sa valeur dans les constructeurs (les champs statiques sont initialisés dans les constructeurs statiques)...

Si cela est vrai (spoiler : c'est le cas) et que le code instancie une nouvelle structure (ligne 1), pourquoi le champ n'est-il pas initialisé ? La réponse courte est que malgré l'expression new, aucun constructeur n'est exécuté, ce qui peut être facilement vérifié en examinant le il généré ci-dessous:

Notez que l'expression 'new S2()'  a été compilée comme l'instruction IL intobj S2 (IL_0002 dans la méthode '<Main>$' mise en évidence dans la capture d'écran ci-dessus). La documentation de cette instruction indique1:

Initialise tous les champs du type de valeur figurant à l'adresse spécifiée en utilisant la référence null ou la valeur 0 du type primitif qui convient...

Contrairement à Newobj, initobj n’appelle pas la méthode du constructeur. Initobj est destiné à l’initialisation des types de valeurs, tandis qu’il newobj est utilisé pour allouer et initialiser des objets.

nous conduisant à la question suivante, naturelle : pourquoi le compilateur a-t-il émis une instruction initobj au lieu d'une newobj?

Si vous prêtez une attention particulière à la déclaration de la structure S2 (lignes 5 à 9), vous remarquerez qu'il n'y a en fait aucun constructeur sans paramètre déclaré, donc la réponse à cette question devient claire : parce que le compilateur ne peut pas invoquer un constructeur non existant, et pour les Value Types, il peut recourir à initobj à la place!

Pour prouver ce point, vous pouvez simplement modifier le code en introduisant un constructeur sans paramètre dans S22:

et observez que maintenant IL_0002 contient l'instruction call instance void S2::.ctor() exécutant effectivement le constructeur sans paramètre sur s2 et d'où, exécutant l'initialisation du champ et provoquant la sortie du programme 32.

Donc en résumé :

  1. Les initialiseurs de champ sont injectés dans les constructeurs du type dans lequel le champ est déclaré ;
  2. Puisqu'aucun constructeur n'est exécuté, tous les champs de structure sont simplement mis à zéro, ce qui explique pourquoi le programme en haut imprime zéro au lieu de 32.

Comme toujours, tous les commentaires sont bienvenus.

Amuse toi!


Structs in C# are fun - Part 4/9: Constructors and struct behavior

Leia este post em português

Lire cet post en français.

  1. Structs in C# are fun.
  2. Brief introduction to Value Types vs Reference Types.
  3. Field initialization in structs.
  4. Constructors and struct behavior (this post).
  5. Other scenarios in which struct constructors behavior may surprise you.
  6. Struct with default argument values in constructors, a.k.a, are you not confused yet?
  7. `required` feature from C# 11 will not save your a** job.
  8. Struct used as default argument values.
  9. Bonus: Struct evolution in C#.

In the previous post of our series on Value Types we presented the code below:

which prints 0 (as opposed to 32 as some would expect) and we've also explored the basics of how field initialization is handled in C#. This post expands that discussion exploring some key relevant aspects that contributes to the disparity between expected/observed behavior.

Let's start with the following statement from the previous post:

In an oversimplified way, whenever C# compiler finds a field initialization it will simply move the initialization code to the constructors, i.e, initializing a field is equivalent to setting its value in the constructors (static fields are initialized in static constructors)...

If that is true (spoiler: it is) and the code is instantiating a new struct (line 1)  why is the field not being initialized ? The short answer is that despite the new expression, no constructor is being run which can be easily verified by looking into the generated il below: 

Note that the expression `new S2()` was compiled as the IL instruction intobj S2 (IL_0002 in method '<Main>$' highlighted in the above screenshot) by the compiler. The documentation for that instruction states1:

Initializes each field of the value type at a specified address to a null reference or a 0 of the appropriate primitive type.

Unlike Newobj, initobj does not call the constructor method. Initobj is intended for initializing value types, while newobj is used to allocate and initialize objects.

leading us to the next, natural, question: why does the compiler emitted a initobj instruction instead of a newobj?

If you pay close attention to the struct declaration (lines 5~9) you'll notice that there are actually no parameterless constructor declared, so the answer to that question becomes clear: because the compiler cannot invoke a non existing constructor, and for value types, it can resort to initobj instead!

To prove that point you can simply change the code, introducing a parameterless constructor in S22:

and observe that now IL_0002 contains the instruction call instance void S2::.ctor() effectively running  the parameterless constructor on s2 and whence, running the field initialization and causing the program to output 32.

So, in summary:

  1. Field initializers are injected into the constructors of the type in which the field is declared;
  2. Since no constructor is executed, all struct fields are simply zeroed out which  explains why the program at the top prints zero instead of 32.

Last, but not least, note that the behavior for classes is different and simply changing the declaration from struct to class (in the original program) leads to a compilation error since the compiler cannot use initobj instruction to initialize a reference type and requires a constructor3 to be available.

As always, all feedback is welcome.

Structs em C# - diversão garantida - Parte 4/9: Comportamento de construtores em estruturas

Read this post in English

Lisez cet post en french.

  1. Structs em C# - diversão garantida
  2. Rápida introdução à Value Types vs Reference Types.
  3. Inicialização de campos em estruturas.
  4. Comportamento de construtores em estruturas (este post).
  5. Outros cenários em que o comportamento de construtores em estruturas podem te surpreender.
  6. Argumentos default  em construtores de estruturas (você ainda não esta confuso ?).
  7. Modificador `required` do C# 11 não vai salvar seu c* trabalho.
  8. Estruturas usadas como valor default de argumentos.
  9. Bonus: Evolução das estruturas em C#. 

No post anterior da nossa série sobre Value Types apresentamos o código abaixo:

o qual imprime 0 (ao contrário de 32 como alguns esperariam) e também exploramos os princípios básicos de como a inicialização de campos é tratada em C#. Este post expande essa discussão explorando alguns aspectos chaves que contribuem para a disparidade entre o comportamento esperado/observado.

Vamos começar com a seguinte declaração do post anterior:

De uma forma simplificada, sempre que o compilador C# encontra a inicialização de um campo o mesmo simplesmente move o código de inicialização para os construtores, ou seja, inicializar um campo é equivalente a definir seu valor nos construtores (campos estáticos são inicializados em construtores estáticos)...

Se isso for verdade (spoiler: é) e o código está instanciando uma nova estrutura (linha 1), por que o campo não está sendo inicializado? A resposta curta é que, apesar da expressão new, nenhum construtor está sendo executado, o que pode ser facilmente verificado observando-se o IL gerado abaixo:

Observe que a expressão C# `new S2()` foi compilada para a instrução IL intobj S2 (IL_0002 no método '<Main>$' destacado na imagem acima). A documentação dessa instrução afirma1:

Inicializa cada campo do tipo de valor em um endereço especificado como uma referência nula ou 0 do tipo primitivo apropriado
...
Ao contrário de Newobj , initobj não chama o método de construtor. Initobj destina-se à inicialização de tipos de valor, enquanto newobj é usado para alocar e inicializar objetos.

levando-nos à próxima pergunta natural: por que o compilador emitiu uma instrução initobj em vez de um newobj?

Se você prestar atenção à declaração da estrutura S2 (linhas 5 a 9), notará que na verdade não existe um construtor sem parâmetros declarado, então a resposta a essa pergunta fica clara: porque o compilador não pode invocar um construtor inexistente e para, Value Types, a opção natural é a utilização da instrução initobj!

Para provar esse ponto você pode simplesmente alterar o código introduzindo um construtor sem parâmetros em S22:

Observe que agora IL_0002 contém a instrução call instance void S2::.ctor(),  efetivamente executando o construtor sem parâmetros em s2 e desta forma a inicialização do campo fazendo com que o programa produza 32 como resultado.

Então, em resumo:

  1. Field Initializers são injetados nos construtores do tipo em que o campo é declarado;

  2. Como no programa exemplo nenhum construtor é executado, todos os campos da struct são simplesmente zerados, o que explica por que o programa imprime zero em vez de 32.

Como sempre, todo feedback é bem vindo.

Divirta-se!


1. This is not 100% accurate since newobj may be used to allocate value types in some scenarios, but that is not important for our discussion.
2. Until version 10, C# did not allow structs to have explicit parameterless constructors.
3. Read more about ctors in C#