Nov 22, 2023

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#

No comments: