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.