Jan 21, 2026

Les structures en C# sont amusantes - Partie 9/9: Conclusion

Leia este post em Português.

Read this post in English.

Dans les articles précédents de cette série, nous avons découvert que, dans certains scénarios, les constructeurs des types valeur (structs) peuvent ne pas être exécutés, malgré une syntaxe qui pourrait laisser penser le contraire.

Être conscient de ce comportement est essentiel, et une attention particulière doit être portée lors de la définition d'API publiques (surtout dans les bibliothèques en raison de leur portée potentielle) afin d'éviter de créer des API contre-intuitives et/ou sujettes aux erreurs, car les développeurs pourraient facilement tomber dans le piège de l'utilisation de membres non initialisés1.

Cela dit, ce comportement n'est pas nouveau ; les problèmes évoqués dans les articles #5, #6 et #7 existent depuis les premières versions du langage. Cependant, l'introduction, en C# 10, de la possibilité de définir des constructeurs sans paramètre dans les structs ajoute une couche de complexité supplémentaire et augmente les chances que les développeurs soient exposés à de tels types2.

Ainsi, si vous consommez des types struct, soyez vigilant afin de ne pas utiliser d'instances non initialisées1. D'un autre côté, faites particulièrement attention lorsque vous définissez de nouveaux types struct, en particulier si vous ajoutez des constructeurs sans paramètres ou un constructeur dont tous les paramètres sont optionnels.

Donc nous arrivons au dernier article de cette série. J'espère que vous avez apprécié ce parcours, que vous y avez appris quelque chose de nouveau et que vous vous sentez désormais mieux préparé(e) à utiliser/définir vos propres structs.

Comme toujours, tout retour est bienvenu.

Amusez-vous bien et à bientôt !


  1. Note de clarification : Du point de vue du runtime, les structs sont garantis d’être initialisés (par une mise à zéro complète de la structure) avant d’être utilisés. Le terme initialisation dans cette série d’articles signifie que tous les champs/propriétés du struct ont reçu des valeurs significatives, laissant ainsi l’instance dans un état cohérent.

  2. Avant C# 10, la seule façon pour un développeur d'être exposé à de tels types était de consommer des assemblages compilés avec un langage prenant en charge cette fonctionnalité, comme IL ; personnellement, je n’ai jamais rencontré un seul exemple de ce genre.

Structs in C# are fun - Part 9/9: Conclusion

Leia este post em Português.

Lire cet post en français.

In the previous posts of this series we've learned that in some scenarios, despite the syntax leading us to believe the contrary, struct constructors may not be executed.

Being aware of this behavior is important and special care should be taken when defining public APIs (specially in libraries due to the potential reach of those) to avoid ending up with APIs that are unintuitive and/or error prone due to developers easily falling into the trap of using uninitialized struct1 members.

That said, this behavior is not new; problems like the ones discussed in post #5, #6 and #7 existed since the first versions of the language; however the introduction, in C# 10, of the ability to define parameterless constructors in structs adds more complexity and makes it more likely that developers will be exposed to such types2.

So, if you are consuming struct types be careful to not end up using uninitialized1 instances. By the other hand, take special care when defining new struct types, specially those with parameterless constructors or a constructor with all of its parameters being optional.

And so we reach the final post in this series. I hope you’ve enjoyed the journey, possibly have learned something new, and now feels better prepared to use and define your own structs.

As always, all feedback is welcome.

Have fun and see you in the next post!


  1. Note of clarification: From the perspective of the runtime, structs are guaranteed to be initialized (by zeroing out the whole struct) before being used. Initialization in the context of this series means that all struct fields/properties have been assigned meaningful values leaving the instance in a consistent/valid state.

  2. Prior to C# 10, the only way a developer would be exposed to such types would be by consuming assemblies built with a language supporting that feature, such as IL; personally I am not aware of a single instance of such types.

Structs em C# - diversão garantida - Parte 9/9: Conclusão

Read this post in English.

Lire cet article en français.

Nos posts anteriores desta série nós aprendemos que, em alguns cenários, construtores de estruturas (structs) podem não ser executados apesar da sintaxe nos levar a acreditar o contrário.

Ter consciência desse comportamento bem como tomar especial cuidado ao definir APIs públicas (principalmente em bibliotecas, já que as mesmas possuem grande potencial de alcance) é essencial para evitar criar APIs contraintuitivas e/ou propensas a erros uma vez que desenvolvedores podem facilmente ser supreendidos ao usar membros de structs não inicializadas1 introduzindo bugs e/ou falhas de segurança em seus programas.

Dito isso, esse comportamento não é novo, e problemas como os discutidos nos posts #5, #6 e #7 existem desde as primeiras versões da linguagem. No entanto, a introdução da capacidade de definir construtores sem parâmetros em structs na versão 10 do C# adiciona uma camada extra de complexidade e torna mais provável que desenvolvedores sejam expostos a tais tipos2.

Portanto, se você usa tipos struct, garanta que todas as instâncias das mesmas sejam corretamente inicializadas1. Por outro, lado preste especial atenção ao definir novos tipos struct, principalmente se os mesmos possuirem construtores sem parâmetros ou um construtor cujos parâmetros sejam todos opcionais.

Com este post chegamos ao último da série; espero que você tenha gostado da mesma e que possa ter aprendido algo novo na jornada e que agora se sinta mais capacitado para utilizar e expor tais estruturas de forma mais eficiente e segura.

Como sempre, qualquer feedback é bem-vindo.

Divirta-se! Nos vemos no próximo post.


  1. Nota de esclarecimento: Do ponto de vista da runtime, structs são sempre inicializadas (zerando toda a estrutura) antes de serem usadas. No contexto desta série de posts o termo inicialização significa que todos os campos/propriedades da struct receberam valores significativos, deixando a instância em um estado consistente/válido.

  2. Antes do C# 10, a única forma de um desenvolvedor ser exposto a esses tipos era consumindo assemblies compilados com uma linguagem que suportasse esse recurso, como IL; pessoalmente, nunca encontrei um exemplo desse tipo.

Mar 1, 2025

Structs in C# are fun - Part 8/9: Struct used as default argument values

Leia este post em Português.

Lire cet post en français.

In a previous post we looked at having default parameter values in struct constructors; this time we are going to have a quick look on what happens when we have struct parameters with default values. So, without further ado lets start with the following struct1:

struct S
{ 
    public int v = 42;
    public S(string s) { v = 13; }
}

and the following usage:

M();

void M(S s = new S()) => Console.WriteLine(s.v);

What you expect the output to be?

If you have been following this post series you probably know that this is a trick question and that neither 42 nor 13 are correct answers since that would require that either the field initializer and/or the constructor to be executed but the expression new S() used as the default parameter value does not implies a constructor invocation (after all there's no such parameterless ctor and if you feel tempted to add one the compiler will happily emit an error since in the presence of such ctor the expression in question does not represent a compile time constant anymore whence it cannot be used as a default parameter value).

When executed that code snippet will output 0 because the compiler simply initializes the full memory used to store the struct instance with zeros (0). By the other hand if you invoke method M() as follows2

M(new S("Foo"));

then the compiler will emit code to execute the constructor and the field initializer and 13 will be printed out, but this has nothing to do with the default parameter value anymore.

And with that we have explored the last not so intuitive struct behavior we intended to cover; next post will be the conclusion of this series.

As always, all feedback is welcome.

Have fun!


  1. Note that support for field initializers in structs was introduced in C# 10.

  2. The content of the string is not important in this context. All that matters is that a constructor is invoked.

Les structures en C# sont amusantes - Partie 8/9: Structure utilisée comme valeurs d'argument défaut

Leia este post em Português.

Read this post in English.

Dans un article précédent, nous avons examiné l'utilisation de valeurs de paramètres par défaut dans les constructeurs de structures ; cette fois, nous allons examiner rapidement ce qui se passe lorsque nous avons des paramètres de structure avec des valeurs par défaut. Alors, sans plus tarder, commençons par la structure suivante[^1] :

struct S
{
    public int v = 42;
    public S(string s) { v = 13; }
}

et l'utilisation suivante :

M();

void M(S s = new S()) => Console.WriteLine(s.v);

Quelle sortie attendez-vous à obtenir ?

Si vous avez suivi cette série d'articles, vous savez probablement qu'il s'agit d'une question piège et que ni 42 ni 13 ne sont les bonnes réponses, car cela nécessiterait que l' initialiseur de champ et/ou le constructeur soient exécutés, mais l'expression new S() utilisée comme valeur de paramètre par défaut n'implique pas un appel de constructeur (après tout, il n'y a pas de constructeur sans paramètre et si vous êtes tenté d'en ajouter un, le compilateur émettra une erreur car en présence d'un tel constructeur, l'expression en question ne représente plus une constante de compilation, d'où elle ne peut pas être utilisée comme valeur de paramètre par défaut).

Lorsqu'il est exécuté, cet extrait de code affichera 0 car le compilateur initialise simplement la mémoire complète utilisée pour stocker l'instance de structure avec des zéros (0). Par contre, si vous appelez la méthode M() comme suit[^2]

M(new S("Foo"));

dans cette cas le compilateur émettra du code pour exécuter le constructeur et l'initialiseur de champ et 13 sera affiché, mais cela n'a plus rien à voir avec la valeur de paramètre par défaut.

Et avec cela, nous avons exploré le dernier comportement de structure pas si intuitif que nous avions l'intention de couvrir ; le prochain article sera la conclusion de cette série.

Comme toujours, tous les commentaires sont les bienvenus.

Amusez-vous bien !


[^1] : Notez que la prise en charge des initialiseurs de champs dans les structures a été introduite dans C# 10.

[^2] : Le contenu de la chaîne n'est pas important dans ce contexte. Tout ce qui compte, c'est qu'un constructeur est appelé.

Structs em C# - diversão garantida - Parte 8/9: Struct usada como valores padrão para argumentos

Read this post in English.

Lire cet article en français.

Em um post anterior examinamos o uso de parâmetros com valores default em construtores de structs; desta vez, vamos dar uma olhada rápida no que acontece quando temos parâmetros do tipo struct com valores default. Então, sem mais delongas, vamos começar com a seguinte struct1:

struct S
{
    public int v = 42;
    public S(string s) { v = 13; }
}

e o seguinte uso:

M();

void M(S s = new S()) => Console.WriteLine(s.v);

Qual saída você espera obter?

Se você tem acompanhado esta série de posts provavelmente notou que esta é uma pergunta capciosa e que nem 42 nem 13 são respostas corretas uma vez que para que tais valores pudessem ser impressos isso exigiria que o inicializador de campo e/ou o construtor fossem executados, mas a expressão new S() usada como valor default em parâmetros não implica uma invocação de construtor (afinal, não há um construtor sem parâmetros e, caso você esteja tentado a adicionar um, o compilador emitirá um erro, pois, na presença de tal construtor, a expressão em questão não representa mais uma constante de tempo de compilação, portanto, não pode ser usada como um valor default).

Na realidade, quando executado o trecho de código exibirá 0 pois o compilador simplesmente inicializa a memória usada para armazenar a instância da struct com zeros (0). Por outro lado, se você invocar o método M() como a seguir2:

M(new S("Foo"));

o compilador emitirá código para executar o construtor e o inicializador de campo e 13 será impresso, mas isso não tem mais nada a ver com o valor de parâmetro padrão.

E com isso exploramos o último comportamento não tão intuitivo de estruturas que pretendíamos abordar; o próximo post será a conclusão desta série.

Como sempre, todos os comentários são bem-vindos.

Divirta-se!


  1. Observe que o suporte para inicializadores de campo em structs foi introduzido no C# 10.

  2. O conteúdo da string não é importante neste contexto. Tudo o que importa é que um construtor seja invocado.

Jan 29, 2025

Les structures en C# sont amusantes - Partie 7/9: Le modificateur *required* du C # 11 ne sauvegardera pas votre emploi

Leia este post em Português.

Read this post in English

L'histoire de ce post est un peu embarrassante.

Pendant le processus de définition des sujets que j'allais couvrir, j'ai découvert une fonctionnalité de C# 11 appelée required members que j'ai naïvement pensé pouvoir utiliser pour signaler ces scénarios, donc j'ai prévu d'ajouter un post montrant comment y parvenir ; cependant, pendant l'investigation/rédaction1 du contenu, j'ai réalisé que ce n'était pas l'un des objectifs de cette fonctionnalité et qu'il y avait plusieurs cas particuliers dans lesquels aucun avertissement ne serait émis même si aucun constructeur ne serait invoqué2.

L'idée principale était de marquer tous les membres (champs/propriétés) qui seraient initialisés dans les constructeurs comme required et d'ajouter l'attribut SetsRequiredMembers à ces constructeurs de manière à ce que dans les cas où aucun constructeur (décoré avec SetRequiredMembersAttribute) ne serait invoqué, le compilateur émettrait un avertissement/erreur en raison de la non-initialisation de ces membres.

Cette technique fonctionne relativement bien si l'on veut détecter un scénario très problématique : l'instanciation de types valeur avec un constructeur dans lequel tous ses paramètres sont optionnels3. Pour rendre la discussion plus concrète, prenons notre dernier exemple du post précédent et modifions-le comme décrit ci-dessus :

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

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

struct S2
{ 
    public required int v;
    
    [System.Diagnostics.CodeAnalysis.SetsRequiredMembers]
    public S2(int i = 42)  => v = i;
}

Avec ce changement en place, au lieu d'obtenir silencieusement une instance de S2 initialisée à zéro (contre la valeur attendue de 42 si le constructeur était invoqué), nous obtenons l'erreur suivante :

error CS9035: Required member 'S2.v' must be set in the object initializer or attribute constructor.

Pas parfait, étant donné que le message sera probablement très confus si l'on s'attend (à tort, mais compréhensible) à ce que le constructeur de S2 soit invoqué, mais il y a d'autres limitations qui rendent cette approche encore moins viable :

  • Incapacité à détecter les constructeurs non invoqués (au moins) dans les expressions default et les instanciations de tableaux.
  • Même dans les scénarios où cela fonctionnerait, il est impossible de garantir qu'un constructeur sera invoqué (par exemple, si nous changeons le code à la ligne #1 en new S2() { v = 5 }, aucun constructeur ne sera invoqué mais aucun avertissement/erreur ne sera émis non plus)

Une alternative plus efficace (si vous déployez votre application en tant qu'application managée plutôt que de la compiler AOT) pour détecter de tels scénarios consiste à vérifier explicitement que les instances de struct ont été initialisées4 (soit par un constructeur, soit par d'autres moyens) avant d'accéder à ses membres ; puisque le code est JITé, on peut implémenter cela de manière à ce que les utilisateurs puissent contrôler si la vérification doit être appliquée ou non et avoir très peu (voire aucun) impact sur les performances lorsque l'application est désactivée, comme démontré ci-dessous.

using System.Runtime.CompilerServices;

class Driver
{
    static void Main()
    {

        for(int i = 0; i < 100_000; i++)
        {
            var foo = new Foo(); // Aucun constructeur invoqué... aucun avertissement :(
            Thread.Sleep(100);
            foo.Use();
        }
    }

}

// Le struct peut être déclaré dans un autre assembly également. 
struct Foo
{
    // il est important que le champ soit marqué comme `readonly`
    private static readonly bool _shouldValidate = Environment.GetEnvironmentVariable("VALIDATE_FOO") == "true";

    private int _i;
    private bool _isInitialized;

    public Foo(int v = 1) { _i = v; _isInitialized = true; }

    public void Use()
    {
        Verify();
        System.Console.WriteLine(_i);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)] // Il est important de demander au JIT d'inliner cette méthode pour que l'optimisation soit appliquée.
    private readonly void Verify()
    {
        if (_shouldValidate)
        {
            if (!_isInitialized)
            {
                throw new Exception("Le constructeur de Foo n'a pas été invoqué ; cela peut être dû à une déclaration de tableau ou ...");
            }
        }
    }
}

Le code utilise le champ _shouldValidate pour contrôler si la vérification de l’initialisation4 doit être appliquée ou non (dans ce cas plus spécifiquement, si le constructeur a été exécuté). Notez que ce champ est déclaré comme static readonly. Cela est très important car avec cela en place, le JIT sait que, pour une instance de struct donnée, une fois initialisée, la valeur du champ ne changera jamais, donc il est libre de traiter _shouldValidate comme une constante et de ne pas générer de code pour le vérifier dans le if à la ligne #XX ; de plus, dans le cas où il est évalué à false, le JIT peut supprimer toute l'instruction if (d'où le surcoût proche de zéro mentionné précédemment).

Vous pouvez voir cette magie du JIT en action en ouvrant un terminal, en créant une application console avec le code ci-dessus et en exécutant :

DOTNET_JitDisasm=Use dotnet run -c Release

ce qui :

  1. sur les systèmes d'exploitation de type unix, définit la variable d'environnement DOTNET_JitDisasm à Use
  2. compile l'application en mode release (-c Release), ce qui est une exigence pour que l'optimisation soit appliquée.
  3. exécute l'application.
  4. demande au JIT de déverser le code assembleur JITé pour la méthode Use() en définissant la variable d'environnement DOTNET_JitDisasm en conséquence.

Lorsque vous exécutez cette ligne de commande, vous devriez voir des zéros (0) et du code assembleur ffiché dans le terminal plusieurs fois. Après quelques itérations, vous devriez être capable de repérer du code assembleur ressemblant à celui ci-dessous (assurez-vous de vérifier celui qui contient Tier1 au lieu de Tier0) :

; Assembly listing for method Foo:Use():this (Tier1)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; Tier1 code
; optimized code
; rsp based frame
; fully interruptible
; No PGO data
; 1 inlinees with PGO data; 0 single block inlinees; 0 inlinees without PGO data

G_M000_IG01:                ;; offset=0x0000
 
G_M000_IG02:                ;; offset=0x0000
       mov      edi, dword ptr [rdi]
 
G_M000_IG03:                ;; offset=0x0002
       tail.jmp [System.Console:WriteLine(int)]
 
; Total bytes of code 8

Ce qui consiste essentiellement à appeler System.Console.WriteLine(_i) et à retourner, sans aucune trace de l’invocation de la méthode Verify().

Vous pouvez également expérimenter cet exemple en l’exécutant comme suit :

DOTNET_JitDisasm=Use VALIDATE_FOO=true dotnet run -c Release

dans ce cas, il lancera une exception (prouvant que l'utilisation d'instances de struct non initialisées sont détectées)

ou

DOTNET_JitDisasm=Use dotnet run -c Debug

dans ce cas, peu importe combien de temps l'application s'exécute, le code assembleur généré pour Use() appellera toujours Verify() (c'est-à-dire que l'optimisation n'a pas été appliquée car l'application a été compilée en mode debug)

Avec cette approche, on peut être sûr qu'aucun code n'utilise d'instances non initialisées en exécutant simplement le code avec la variable d'environnement définie à true et en observant les exceptions.

Comme toujours, tous les commentaires sont les bienvenus.

Amusez-vous bien !


  1. ici vous pouvez trouver du code de test que j'ai utilisé en explorant ce sujet.

  2. Après avoir réalisé cela, j'ai changé le titre du post :).

  3. Ce cas particulier est problématique en raison de l'attente que le comportement corresponde au comportement pour les classes

  4. Note de clarification : Du point de vue du runtime, les structs sont garantis d'être initialisés (en mettant à zéro tout le struct) avant d'être utilisés. Initialisation dans le contexte de cette série de posts signifie que tous les champs/propriétés du struct ont été assignés à des valeurs significatives, laissant l'instance dans un état cohérent.

Structs em C# - diversão garantida - Parte 7/9: A funcionalidade *required members* do C# 11 não vai salvar seu emprego

Lire cet post en français.

Read this post in English

A história deste post é um pouco embaraçosa.

Durante o processo de definição dos tópicos que eu cobriria, aprendi sobre uma funcionalidade do C# 11 chamada required members que, ingenuamente, pensei poderia ser usada para sinalizar esses cenários, então planejei adicionar um post mostrando como alcançar isso; no entanto, durante a investigação/redação1 do conteúdo percebi que esse não era um dos objetivos dessa funcionalidade e que havia vários casos extremos em que nenhum aviso seria emitido, mesmo que nenhum construtor fosse invocado2.

A ideia principal era marcar todos os membros (campos/propriedades) que seriam inicializados nos construtores como required e adicionar o atributo SetsRequiredMembers a esses construtores, de forma que, nos casos em que nenhum construtor (decorado com SetRequiredMembersAttribute) fosse invocado, o compilador emitiria um aviso/erro devido à não inicialização desses membros.

Essa técnica funciona relativamente bem se quisermos capturar um cenário muito problemático: a instanciação de tipos de valor com um construtor em que todos os seus parâmetros possuim valores opcionais3. Para tornar a discussão mais concreta, vamos pegar nosso último exemplo do post anterior e modificá-lo conforme descrito acima:

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

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

struct S2
{ 
    public required int v;
    
    [System.Diagnostics.CodeAnalysis.SetsRequiredMembers]
    public S2(int i = 42)  => v = i;
}

Com essa mudança em vez de obter silenciosamente uma instância de S2 inicializada com zero (em oposição ao esperado 42 se o construtor fosse invocado), o compiladore emite o seguinte erro:

error CS9035: O membro obrigatório 'S2.v' deve ser definido no inicializador de objeto ou no construtor de atributo.

Não é perfeito, já que a mensagem provavelmente será muito confusa se alguém estiver (incorretamente, mas compreensivelmente) esperando que o construtor de S2 seja invocado, mas há outras limitações que tornam essa abordagem ainda menos viável:

  • Incapacidade de detectar construtores não invocados (pelo menos) em expressões default e instanciações de arrays.
  • Mesmo em cenários em que isso funcionaria, é impossível garantir que um construtor será invocado (por exemplo, se mudarmos o código na linha #1 para new S2() { v = 5 }, nenhum construtor será invocado, mas nenhum aviso/erro será emitido)

Uma alternativa mais eficaz (se você implantar sua aplicação como gerenciada em vez de compilá-la AOT) para detectar tais cenários é garantir explicitamente que instâncias de struct foram inicializadas4 (seja por um construtor ou por outros meios) antes de acessar seus membros; como o código está gerado em tempo de execução pelo JIT, é possível implementar isso de forma que os usuários possam controlar se tal verificação deve ser aplicada ou com muito pouco (ou nenhum) impacto em desempenho quando a verificação está desativada, como demonstrado abaixo.

using System.Runtime.CompilerServices;

class Driver
{
    static void Main()
    {

        for(int i = 0; i < 100_000; i++)
        {
            var foo = new Foo(); // Nenhum construtor invocado... nenhum aviso :(
            Thread.Sleep(100);
            foo.Use();
        }
    }

}

// A estrutura pode ser declarada em um assembly diferente também. 
struct Foo
{
    // é importante que o campo seja marcado como `readonly`
    private static readonly bool _shouldValidate = Environment.GetEnvironmentVariable("VALIDATE_FOO") == "true";

    private int _i;
    private bool _isInitialized;

    public Foo(int v = 1) { _i = v; _isInitialized = true; }

    public void Use()
    {
        Verify();
        System.Console.WriteLine(_i);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)] // É importante pedir que o JIT inline este método para que a otimização seja aplicada.
    private readonly void Verify()
    {
        if (_shouldValidate)
        {
            if (!_isInitialized)
            {
                throw new Exception("O construtor de Foo não foi invocado; isso pode ser devido a uma declaração de array ou ...");
            }
        }
    }
}

O código usa o campo _shouldValidate para controlar se a verificação de inicialização4 deve ser aplicada ou não (neste caso, mais especificamente, se o construtor foi executado). Observe que o mesmo é declarado como static readonly; isso é muito importante pois assim o JIT sabe que, uma vez inicializada uma dada instância de struct, o valor do campo nunca mudará, podendo tratar _shouldValidate como uma constante não gerando código para verificá-lo no if na linha #XX; além disso, caso esta constante seja avaliada como false, o JIT pode remover o if (incluindo seu corpo) completamente (daí o custo quase zero mencionado anteriormente).

Você pode ver essa mágica do JIT em ação abrindo um terminal, criando uma aplicação de console com o código acima e executando:

DOTNET_JitDisasm=Use dotnet run -c Release

o qual:

  1. em sistemas operacionais do tipo unix, define a variável de ambiente DOTNET_JitDisasm como Use, o que instrui o JIT a fazer o dump do código assembly gerado para o método de mesmo nome.
  2. compila a aplicação em modo release (-c Release), o que é um requisito para que a otimização seja aplicada.
  3. executa a aplicação.

Ao executar DOTNET_JitDisasm=Use dotnet run -c Release, você deve ver zeros (0) e algum código assembly sendo impresso no terminal várias vezes; após algumas iterações, você deve ser capaz de identificar código assembly semelhante ao abaixo (certifique-se de verificar o que mesmo contém Tier1 em vez de Tier0):

; Assembly listing for method Foo:Use():this (Tier1)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; Tier1 code
; optimized code
; rsp based frame
; fully interruptible
; No PGO data
; 1 inlinees with PGO data; 0 single block inlinees; 0 inlinees without PGO data

G_M000_IG01:                ;; offset=0x0000
 
G_M000_IG02:                ;; offset=0x0000
       mov      edi, dword ptr [rdi]
 
G_M000_IG03:                ;; offset=0x0002
       tail.jmp [System.Console:WriteLine(int)]
 
; Total bytes of code 8

que basicamente chama System.Console.WriteLine(_i) e retorna, sem nenhum traço da invocação do método Verify().

Você também pode brincar com este exemplo executando-o como:

DOTNET_JitDisasm=Use VALIDATE_FOO=true dotnet run -c Release

nesse caso, ele lançará uma exceção (provando que o uso de instâncias de struct não inicializadas é detectado)

ou

DOTNET_JitDisasm=Use dotnet run -c Debug

não importando nesse caso quanto tempo a aplicação seja executada, o código assembly gerado para Use() sempre chamará Verify() (ou seja, a otimização não foi aplicada porque a aplicação foi compilada em modo debug)

Com essa abordagem, pode-se ter certeza de que nenhum código está usando instâncias não inicializadas simplesmente executando o código com a variável de ambiente definida como true e observando exceções.

Como sempre, todos os feedbacks são bem-vindos.

Divirta-se!


  1. aqui você pode encontrar alguns códigos de teste que usei enquanto explorava este tópico.

  2. Depois de perceber isso, mudei o título do post :).

  3. Este caso específico é problemático devido à expectativa de que o comportamento corresponderia ao comportamento para classes.

  4. Nota de esclarecimento: Do ponto de vista da runtime, structs são garantidas de serem inicializados (zerando toda a struct) antes de serem usadas. Inicialização no contexto desta série significa que todos os campos/propriedades da struct foram atribuídos valores significativos, deixando a instância em um estado consistente.

Jan 2, 2025

Structs in C# are fun - Part 7/9: Required feature from C# 11 will not save your a** job.

Leia este post em Português.

Lire cet post en français.

The history of the current post is a little bit embarrassing.

During the process of defining the topics I'd cover I've learned about a C# 11 feature called required members which I naively though could be used to flag these scenarios so I planned to add a post showing how to achieve that; however during investigation/drafting1 the content, I've realized that that was not one of the goals of this feature and that there were multiple corner cases in which no warning would be emitted even though no constructor would be invoked2.

The main idea would be to to mark all members (field/properties) that would be initialized in the constructors as required and add the SetsRequiredMembers attribute to these constructors in such a way that in cases where no constructors (decorated with SetRequiredMembersAttribute) were to be invoked the compiler would emit a warning/error due to the non-initialization of such members.

This technique works relatively well if one wants to catch a very problematic scenario: instantiation of value types with a constructor in which all of its parameters are optional3. To make the discussion more concrete, lets take our last example from the previous post and modify it as described above:

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

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

struct S2
{ 
    public required int v;
    
    [System.Diagnostics.CodeAnalysis.SetsRequiredMembers]
    public S2(int i = 42)  => v = i;
}

With this change in place, instead of silently getting an instance of S2 initialized with zero (as opposed to the expected 42 if the constructor were to be invoked), we get the following error:

error CS9035: Required member 'S2.v' must be set in the object initializer or attribute constructor.

Not perfect, given that the message will probably be very confusing if one is (incorrectly, but understandably) expecting S2 constructor to be invoked, but there are other limitations rendering this approach even less viable:

  • Inability to detect constructors not being invoked (at least) in default expression and array instantiations.
  • Even in scenarios in which this would work it is impossible to guarantee that a ctor will be invoked (for instance, if we change the code at line #1 to new S2() { v = 5 }, no constructor will be invoked but no warning/error will be emitted either)

A more effective alternative (if you deploy your application as a managed one as opposed to AOTing it) to detect such scenarios is by explicitly asserting that struct instances have been initialized4 (either by a constructor or some other means) prior to accessing its members; since the code is being JITed one can implement this in a way users can control whether the check should be enforced or not and have very little (if any) performance impact when enforcing is disabled, as demonstrated below.

using System.Runtime.CompilerServices;

class Driver
{
    static void Main()
    {

        for(int i = 0; i < 100_000; i++)
        {
            var foo = new Foo(); // No constructor invoked... no warnings :(
            Thread.Sleep(100);
            foo.Use();
        }
    }

}

// The struct can be declared in a different assembly also. 
struct Foo
{
    // it is important for the field to be marked as `readonly`
    private static readonly bool _shouldValidate = Environment.GetEnvironmentVariable("VALIDATE_FOO") == "true";

    private int _i;
    private bool _isInitialized;

    public Foo(int v = 1) { _i = v; _isInitialized = true; }

    public void Use()
    {
        Verify();
        System.Console.WriteLine(_i);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)] // It is important to ask the JIT to inline this method for the optimization to be applied.
    private readonly void Verify()
    {
        if (_shouldValidate)
        {
            if (!_isInitialized)
            {
                throw new Exception("Foo constructor was not invoked; this may be due an array declaration or ...");
            }
        }
    }
}

The code use the field _shouldValidate to control whether correct initialization4 should be enforced or not (in this case more specifically, if the constructor has been executed). Notice that this field is declared as static readonly; this is very important since with this in place the JIT knowns that, for a given struct instance, once initialized, the field value will never change so it is free to handle _shouldValidate as a constant and to not generate code to check it in the if in line #XX; moreover, in case it is evaluated to false the JIT can remove the whole if statement (whence the close to zero overhead mentioned before).

You can see this JIT magic in action by opening a terminal, creating a console application with the code above and running:

DOTNET_JitDisasm=Use dotnet run -c Release

which:

  1. in unix like OSs, sets the environment variable DOTNET_JitDisasm value to Use and runs dotnet run -c Release.
  2. builds the application in release mode (-c Release), which is requirement for the optimization to be applied.
  3. instructs the JIT to dump the JITed assembly code for the method Use() by setting DOTNET_JitDisasm environment variable accordingly.

When executing that command line you should see zeros (0) and some assembly code being printed to the terminal multiple times; after some iterations you should be able to spot some assembly code resembling the one below (make sure to check the one that contains Tier1 as opposed to Tier0):

; Assembly listing for method Foo:Use():this (Tier1)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; Tier1 code
; optimized code
; rsp based frame
; fully interruptible
; No PGO data
; 1 inlinees with PGO data; 0 single block inlinees; 0 inlinees without PGO data

G_M000_IG01:                ;; offset=0x0000
 
G_M000_IG02:                ;; offset=0x0000
       mov      edi, dword ptr [rdi]
 
G_M000_IG03:                ;; offset=0x0002
       tail.jmp [System.Console:WriteLine(int)]
 
; Total bytes of code 8

which is basically calling System.Console.WriteLine(_i) and returning, with no traces of the method Verify() invocation.

You can also play around with this example by running it as:

DOTNET_JitDisasm=Use VALIDATE_FOO=true dotnet run -c Release

in which case it will throw an exception (proving that uninitialized struct instances usage is detected)

or

DOTNET_JitDisasm=Use dotnet run -c Debug

in which case, no matter for how long the application runs, the assembly code generated for Use() will always call Verify() (i.e. optimization was not applied because application was built in debug mode)

With this approach one can be sure that no code is using uninitialized instances by simply running the code with the environment variable set to true and observing for exceptions.

As always, all feedback is welcome.

Have fun!


  1. here you can find some test code I used while exploring this topic.

  2. After realizing that I've changed the post's title :).

  3. This particular case is problematic due to the expectation that the behavior would match the behavior for classes

  4. Note of clarification: From the perspective of the runtime, structs are guaranteed to be initialized (by zeroing out the whole struct) before being used. Initialization in the context of this series of posts means that all struct fields/properties have been assigned meaningful values leaving the instance in a consistent state.

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.

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

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.