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.