Jan 31, 2024

Les structures en C# sont amusantes - Partie 6/9: Struct avec des valeurs d'argument par défaut dans les constructeurs, ou, n'êtes-vous pas encore confus ?

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.
  6. Struct avec des valeurs d'argument par défaut dans les constructeurs, ou, n'êtes-vous pas encore confus ? (cet post)
  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 les derniere posts, nous avons vu que le compilateur C# peut net pas exécuter le constructeur des structs dans certains scénarios. Malheureusement, l'expérience avec les constructeurs dans les structures, du moins à mon avis, peut devenir encore plus déroutante.

Pour illustrer mon point, supposons que vous ayez le code suivant :

Print(new S2()); 
Print(new S2(13));

void Print(S2 s) => System.Console.WriteLine(s.v);

struct S2
{ 
    public int v;
    public S2(int i = 42)  => v = i;
    public S2() => v = 84;
}

Quel résultat attendez-vous s’il est compilé/exécuté1 ?

  1. Le compilateur émettra une erreur en affirmant que new S2() est un appel ambigu.
  2. Il compile et génère 42, 13.
  3. Il compile et génère 84, 13.

La bonne réponse, qui peut surprendre certains développeurs, est qu'il se compile avec succès et affiche 84 et 13 (c'est-à-dire la troisième option).

Cela se produit parce qu'à la ligne 1, le compilateur C# considère le constructeur sans paramètre comme une meilleure correspondance (better match) que celui avec la valeur de paramètre par défaut excluant les options 1 et 21. Le bon côté est que même si cela n'est pas totalement évident, au moins ce comportement est cohérent entre les classes/structures.

Pourtant, avec les structures, cela peut devenir encore plus complexe/déroutant ; imaginez que vous avez exactement le même code que ci-dessus, la seule différence étant la suppression du constructeur sans paramètre :

Print(new S2()); 
Print(new S2(13));

void Print(S2 s) => System.Console.WriteLine(s.v);

struct S2
{ 
    public int v;
    public S2(int i = 42)  => v = i;
}

maintenant, c'est sûr qu'il imprimera 42, 13, n'est-ce pas ?

Non! Dans ce scénario, contrairement aux classes, un constructeur avec des valeurs par défaut pour tous ses paramètres n'est pas invoqué, même sur une new expression explicite2, ce qui signifie que le code ci-dessus imprimera 0 et 133.

Dans le prochain article, nous examinerons rapidement les membres requis C# 114 comme moyen d'aider à identifier les scénarios dans lesquels aucun constructeur n'est invoqué.

Comme toujours, tous les commentaires sont les bienvenus.

Amusez-vous!


  1. Pour être honnête, le comportement ici est le même pour les classes, donc au moins il est cohérent.

  2. la spécification des constructeurs sans paramètre dans les structures indique explicitement que ce comportement est le même que dans les versions précédentes de C#.

  3. Cela se produit parce que la ligne n°1 est équivalente à Print(default(S2)) ; pour plus de détails, voir cet article.

  4. Je suis conscient que ce n'est pas l'utilisation prévue de cette fonctionnalité, mais elle peut être utilisée pour signaler quelques instanciations de structure qui n'invoquent pas de constructeur.

Structs in C# are fun - Part 6/9: Struct with default argument values in ctors a.k.a, are you not confused yet ?

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
  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?(this post)
  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 we saw that the C# compiler may not emit a ctor invocation in some scenarios. Unfortunately the experience with constructors in structs, at least in my opinion, may get even more confusing.

To illustrate this point, suppose you have the following code:


Print(new S2()); 
Print(new S2(13));

void Print(S2 s) => System.Console.WriteLine(s.v);

struct S2
{ 
    public int v;
    public S2(int i = 42)  => v = i;
    public S2() => v = 84;
}

What do you expect to happen when you compile/run this code1?

  1. Compiler will emit an error claiming new S2() is an ambiguous call.
  2. It compiles and outputs 42, 13.
  3. It compiles and outputs 84, 13.

The correct answer, which may surprise some developers, is that it compiles successfully and prints 84 and 13 (i.e, the third option).

That happens because in line 1 the C# compiler sees the parameterless constructor as a better match than the one with the default parameter value ruling out options 1 and 21. The bright side is that even not being totally obvious, at least this behavior is consistent across classes/structs,

However, with structs, it may get even more complex/confusing; imagine you have exactly the same code as above, with the only difference being the removal of the parameterless constructor:

Print(new S2()); 
Print(new S2(13));

void Print(S2 s) => System.Console.WriteLine(s.v);

struct S2
{ 
    public int v;
    public S2(int i = 42)  => v = i;
}

now, for sure it will print 42, 13, right ?

Nope! In this scenario, differently from classes, a constructor with default values for all of its parameters is not invoked, even on an explicit new expressions2 which means the code above will print 0 and 133.

In the next post we'll take a quick look into required members C# 11 feature4 as a way to help pin-pointing scenarios in which no constructor is being invoked.

As always, all feedback is welcome.

Have fun!


  1. To be fair the behavior here is the same for classes, so at least it is consistent

  2. The specification of parameterless constructors in structs explicitly states that this behavior is the same as in previous versions of C#

  3. That happens because line #1 is equivalent to Print(default(S2)); for more details see this post.

  4. I am aware that this is not the intended use of this feature but it may be used to flag struct instantiations that does not invoke a constructor.

Structs em C# - diversão garantida - Parte 6/9: Argumentos default em construtores de estruturas (você ainda não esta confuso ?)

Lire cet post en français.

Read this post in english

  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.
  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 ?). (este post)
  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#.

Nos posts anteriores vimos que, em alguns cenários, o compilador C# pode não emitir invocaçcões de construtores de estruturas. Infelizmente a experiência com construtores em structs, pelo menos na minha opinião, pode ficar ainda mais confusa.

Para ilustrar esse ponto, suponha que você tenha o seguinte código:


Print(new S2()); 
Print(new S2(13));

void Print(S2 s) => System.Console.WriteLine(s.v);

struct S2
{ 
    public int v;
    public S2(int i = 42)  => v = i;
    public S2() => v = 84;
}

Qual resultado você espera observar ao compilar/executar o mesmo? 1

  1. O compilador emitirá um erro informando que new S2() representa uma chamada ambígua.
  2. Ele compila e gera 42, 13.
  3. Ele compila e gera 84, 13.

A resposta correta, que pode surpreender alguns desenvolvedores, é que o programa compila com sucesso e imprime 84 e 13 (ou seja, a terceira opção).

Isso acontece porque na linha 1 o compilador C# vê o construtor sem parâmetros como uma melhor correspondência (best match) que aquele com o valor do parâmetro default, descartando as opções 1 e 21. O lado bom é que mesmo não sendo totalmente óbvio, pelo menos este comportamento é consistente entre classes/estruturas,

No entanto, o comportamento de construtores com relação à estruturas pode ficar ainda mais complexo/confuso; imagine que você tem exatamente o mesmo código acima, com a única diferença sendo a remoção do construtor sem parâmetros:

Print(new S2()); 
Print(new S2(13));

void Print(S2 s) => System.Console.WriteLine(s.v);

struct S2
{ 
    public int v;
    public S2(int i = 42)  => v = i;
}

agora, com certeza o mesmo vai imprimir 42 e 13, certo?

Não! Neste cenário, diferentemente das classes, um construtor com valores padrão para todos os seus parâmetros não é invocado, mesmo em expressões new explícitas2 o que significa que o código acima irá imprimir 0 e 133.

Na próxima postagem, daremos uma olhada rápida em uma funcionalide do C# 11 conhecida como required members4 como uma forma de ajudar a identificar cenários nos quais nenhum construtor é invocado.

Como sempre, todo feedback é bem-vindo.

Divirta-se!


  1. Para ser justo, o comportamento aqui é consistente com o de classes.

  2. A especificação de construtores sem parâmetros em estruturas afirma explicitamente que esse comportamento é o mesmo das versões anteriores do C#.

  3. Isso acontece porque a linha #1 é equivalente a Print(default(S2)); para obter mais detalhes, consulte esta postagem.

  4. Estou ciente de que este não é o uso pretendido deste recurso, contudo o mesmo pode ser usado para sinalizar algumas instanciações de estruturas que não invocam um construtor.

Dec 22, 2023

Structs em C# - diversão garantida - Parte 5/9: Outros cenários em que o comportamento de construtores em estruturas podem te surpreender.

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.
  5. Outros cenários em que o comportamento de construtores em estruturas podem te surpreender (este post).
  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 apresentamos um programa no qual struct contructors não erão invocados não importando como a sintaxe nos levasse a acreditar no contrário. Neste post  continuamos a discussão sobre construtores em Value Types adicionando mais alguns cenários mostrados no código abaixo:

Dado o código acimae, qual valores você espera que sejam mostrados? 42, 42, -1, 42?

Baseado no conhecimento do post anterior sabemos que a inicialização da quarta variável (marcada como // 4) introduzirá uma chamada ao construtor sem parâmetros assim s_4.v será inicializado com o valor 42; mas e quanto aos outros 3 casos?

Se inspecionarmos o código IL gerado notaremos que o compilador emitiu uma chamada ao constructor  S(int) para a variável s_3 mas que nenhum construtor foi executado na inicialização das duas primeiras variáveis o que implica que  s_1 e s_2 terão o campo v inicializado com 0 fazendo com que código acima imprima 0, 0, -1, 42.

Porque ?

Poderíamos argumentar que nos quatro casos um value type está sendo instanciado e que, assim sendo, nos quatro casos um construtor deveria ser executado. Para tentar compreender porque nenhum construtor foi executado para as duas primeiras variáveis vamos recorrer à especificação da linguagem C#.

Começando com s_1, a documentação de default expression diz:

Uma expressão de valor padrão é usada para obter o valor padrão (valores padrão) de um tipo.

referenciando a sessão valores padrão (Default Values), onde podemos ler:

Para uma variável de um value_type, o valor padrão é o mesmo que o valor calculado pelo construtor padrão do Value_type(construtores padrão).

que por sua vez referencia a sessão construtores padrão (Default Constructors) onde finalmente podemos ler:

Todos os tipos de valor declaram implicitamente um construtor de instância sem parâmetros públicos chamado de construtor padrão. O construtor padrão retorna uma instância inicializada em zero conhecida como valor padrão para o tipo de valor:
...
  • Para um struct_type, o valor padrão é o valor produzido definindo todos os campos de tipo de valor como seu valor padrão e todos os campos de tipo de referência como null.
de forma que mesmo mencionando a execução de construtores padrão ela também define que o construtor padrão para um value type1 é equivalente a inicializar a memória reservada para a variável com 0 (zero). Assim sendo não executar tais construtores é o comportamento experado neste cenário.

Neste ponto nos resta apenas um último cenário: arrays de value types. Para compreender o mesmo vamos mais uma vez analisar o que a especificação da linguágem diz a respeito da instanciação de array:

As instâncias de matriz são criadas por array_creation_expressions (expressões de criação de matriz) ou por declarações de campo ou de variável local que incluem um array_initializer (inicializadores de matriz).....

Os elementos das matrizes criados por array_creation_expression s são sempre inicializados para seu valor padrão (valores padrão).

o que significa que quando um array é instanciado todos seus elementos são inicializados com seus respectivos valores default que no caso de value types, como vimos acima, é equivalente a inicializar a memória reservada para a variável com 0 (zeros).

Acredito que este comportamento tenha 2 principais motivos: i) historicamente C# introduziu suporte a declaração explicita de construtores sem parâmetros em value types apenas na versão 102 da linguágem e, ii) performance: imagine um programa alocando um array como new S[1_000_000]; se constructores padrão tivessem que ser executados, instanciação de arrays de value types poderiam apresentar um grande impacto em performance.

Como sempre, todo feedback é bem vindo.

Divirta-se!


1. Observe que mesmo com a introdução do suporte a declaração explícita de construtores padrão (sem parâmetros) no C# 10, nada muda com relação a default expressions para value types, ou seja, mesmo que um value type (VT) declare um construtor sem parâmetos
default(VT) irá produzir uma instância inicializada com 0 (zero) e nenhum construtor será invocado (provavelmente a especificação da linguagem será modificada para tornar este ponto mais claro).

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.

Structs in C# are fun - Part 5/9: Other scenarios in which struct constructor behavior may surprise you

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
  5. Other scenarios in which struct constructors behavior may surprise you (this post)
  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 I've presented a scenario in which a struct ctor would not be invoked, no matter how the syntax may lead us to believe one would be. This post expands on that, adding a couple more of those scenarios shown in the code below:

Given the code above, what do you expect to be printed ? 42, 42, -1, 42?

With the knowledge from the previous post we know that the fourth invocation (marked as // 4) will invoke the parameterless constructor whence, s_4.v will have a value of 42; but what about the other 3 cases? 

If we inspect the generated IL we'll see that the compiler emitted a call to S(int) constructor for s_3 but no constructors were invoked for the two first cases meaning that s_1 and s_2 will have its v field initialized to 0, so the code above will print 0, 0, -1, 42.

Why ?

One could argue that in all 4 scenarios a value type is being instantiated and whence a constructor should have been executed, so lets dig a little more on the 2 scenarios above for which no constructor is invoked and try to reason about the motivation  behind the decision to do so.

Starting with s_1, if we look into the documentation for the default expression we can read:

A default value expression is used to obtain the default value (§9.3) of a type.

referencing section 9.3 Default Values where we can read:

For a variable of a value_type, the default value is the same as the value computed by the value_type’s default constructor (§8.3.3).

which itself references section 8.3.3 Default Constructors where we can finally read:

All value types implicitly declare a public parameterless instance constructor called the default constructor. The default constructor returns a zero-initialized instance known as the default value for the value type:
...
  • For a struct_type, the default value is the value produced by setting all value type fields to their default value and all reference type fields to null.

so, even though the documentation mentions invoking the default constructor to obtain the default value it also says that the default constructor for a value type is equivalent to zeroing out all the memory reserved for the type1 so, no constructor being invoked in this scenario is the expected behavior.

This leaves us with our last scenario: arrays of value types. To tackle that one lets resort again on what language specification says about array creation:

Array instances are created by array_creation_expressions (§12.8.16.5)....

Elements of arrays created by array_creation_expressions are always initialized to their default value (§9.3).

which means that when an array is instantiated all of its elements are set to the default value of the array type, which for value types, as we saw above, is the equivalent of zeroing out the allocated memory.

I believe that this behavior of not invoking the default constructor on value types has two main motivations: i) historically C# did not supported the concept of explicit parameterless (default) constructors on value types until C# 102, and ii) performance: imagine some code doing something like new S[1_000_000]; if default constructors were to be executed this could take an unpredictable amount of time.

As always, all feedback is welcome.

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!