Traduction

Ceci est la traduction la plus fidèle possible de l'article de Jon Skeet, C# and beforefieldinitC# and beforefieldinit.

Différences entre les constructeurs statiques et les initialiseurs de type

Certaines implémentations du pattern singleton dépendent du comportement des constructeurs statiques et des initialiseurs de type, en particulier par rapport au moment où ils sont invoqués.

La spécification de C#

Le constructeur statique d'une classe est exécuté au plus une fois dans un domaine d'application donné. L'exécution d'un constructeur statique est déclenchée par le premier des événements suivants dans un domaine d'application :
- une instance de la classe est créée ;
- l'un des membres statiques de la classe est référencé.

La spécification du CLI (ECMA 335) à la section 8.9.5 :

1. Un type peut avoir une méthode pour l'initialisation de type, ou pas ;
2. Un type peut être spécifié comme ayant une sémantique laxiste au niveau de sa méthode pour l'initialisation de type (par facilité, nous appellerons ci-dessous cette sémantique laxiste BeforeFieldInit) ;
3. Si BeforeFieldInit est indiqué, alors la méthode d'initialisation de type est exécutée aupremier accès ou parfois avant, et à n'importe quel champ défini pour ce type ;
4. Si BeforeFieldInit n'est pas indiqué alors cette méthode d'initialisation de type est exécutée (i.e., est déclenché par) :
- au premier accès à n'importe quel champ statique ou d'instance de ce type,
- ou à la première invocation de n'importe quelle méthode statique, d'instance ou virtuel de ce type.

La spécification C# indique qu'aucun type avec des constructeurs statiques ne devrait être marqué avec l'attribut beforefieldinit. En effet, ceci est confirmé par le compilateur mais avec un effet quelque peu surprenant. Je suspecte que beaucoup de programmeurs pensent (comme je l'ai fait pendant longtemps) que les classes suivantes étaient équivalentes sémantiquement parlant :

 
Sélectionnez
class Test
{
    static object o = new object();
}

class Test
{
    static object o;

    static Test()
    {
        o = new object();
    }
}

Les deux classes ne sont en fait pas les mêmes. Elles ont toutes deux un initialiseur de type - et les deux initialiseurs de types sont les mêmes. Cependant, la première classe n'a pas de constructeur statique, alors que la seconde en a un. Cela signifie que la première classe peut être marquée avec beforefieldinit et a son initialiseur de type invoqué à n'importe quel moment avant la première référence au champ statique. Le constructeur statique n'a même rien à faire. Cette troisième classe est équivalente à la seconde :

 
Sélectionnez
class Test
{
    static object o = new object();

    static Test()
    {
    }
}

Je pense que ceci est une source de confusion significative - particulièrement au niveau des implémentations de singleton.

La nature curieurse de beforefieldinit - Différé ou non ?

L'attribut beforefieldinit a un effet étrange, en fait, il ne signifie pas seulement qu'un initialiseur de type est invoqué plus tôt qu'un type équivalent sans cet attribut - il pourrait même être invoqué plus tard, ou pas du tout. Considérons le programme suivant :

 
Sélectionnez
using System;

class Test
{
    public static string x = EchoAndReturn ("In type initializer");

    public static string EchoAndReturn (string s)
    {
        Console.WriteLine (s);
        return s;
    }
}

class Driver
{
    public static void Main()
    {
        Console.WriteLine("Starting Main");
        // Invoke a static method on Test
        Test.EchoAndReturn("Echo!");
        Console.WriteLine("After echo");
        // Reference a static field in Test
        string y = Test.x;
        // Use the value just to avoid compiler cleverness
        if (y != null)
        {
            Console.WriteLine("After field access");
        }
    }
}

Le résultat de l'exécution est assez varié. Le runtime pourrait décider de démarrer l'initialiseur de type au chargement d'un assembly pour démarrer avec :

 
Sélectionnez

In type initializer
Starting Main
Echo!
After echo
After field access

Ou peut-être, il le démarrera quand la méthode statique sera lancée la première fois...

 
Sélectionnez
Starting Main
In type initializer
Echo!
After echo
After field access

Ou même il attendra jusqu'à ce que le champ soit accédé pour la première fois...

 
Sélectionnez
Starting Main
Echo!
After echo
In type initializer
After field access

(En théorie, l'initialiseur de type pourrait même être lancé après l'affichage de "Echo!" mais avant le message "After echo". Je serais cependant vraiment surpris de voir un runtime avoir ce comportement.) Avec un constructeur statique dans Test, seulement le second résultat serait possible. Beforefieldinit peut différer l'invocation de celle de l'initialiseur de type (le dernier résultat) ou être très rapide (le premier résultat). Je pense même que les développeurs qui connaissent l'existence de beforefieldinit peuvent être assez surpris. La documentation MSDN de TypeAttributesBeforeFieldInit est particulièrement pauvre à ce sujet. Elle décrit l'attribut comme ceci :

« Spécifie que l'appel à des méthodes statiques du type ne force pas le système à initialiser le type. »

Alors que ceci est vrai dans le sens le plus stricte possible, ce n'est certainement pas l'entière vérité - cela suggère que l'attribut peut seulement rendre l'initialisation différée et non rapide.

Il est intéressant de noter que la CLR v4 se comporte différemment par rapport aux CLRs v1 et V2 ici - chacune obéit à sa spécification mais la CLR v4 est vraiment différée dans beaucoup de cas alors que les versions plus récentes sont plus rapides.

Que faudrait-il faire ?

Je propose les changements suivants :

  • les initialiseurs de champs statiques devraient être traités comme s'ils faisaient partie d'un constructeur statique. En d'autres mots, n'importe quel type avec un initialiseur statique ou un constructeur statique explicite ne devrait pas (par défaut) être marqué comme beforefieldinit
    (modification pour la spécification de C#) ;
  • il devrait y avoir un moyen de surcharger ce comportement par défaut au niveau du code. Un attribut serait une solution parfaitement raisonnable pour ce cas (modification pour la spécification de C# et ajout d'un attribut à la librairie standard) ;
  • La documentation pour TypeAttributes.BeforeFieldInit devrait être clarifiée de manière significative(modification pour la documentation MSDN et ECMA 335).

Réflexions supplémentaires (suite à des échanges dans des groupes de discussion)

La première des propositions ci-dessus est définitivement la plus sujette à controverse (la dernière n'est pas controversable du tout, pour autant que je puisse en juger). La raison est la performance. En réalité, peu de classes ont besoin de ce comportement supposé par beaucoup de développeurs - la plupart des gens n'ont jamais besoin de savoir la réelle différence. Cependant le compilateur JIT s'en soucie beaucoup : si un membre statique est utilisé à l'intérieur d'une boucle rapide de par sa simplicité, cela a plus de sens d'initialiser le type avant d'entrer dans la boucle, sachant par la suite que le type a déjà été initialisé. Lorsque le code est partagé par différents domaines d'application, je suis convaincu que ceci devient encore plus important.

Faire que la recompilation de code existant avec une nouvelle version du framework puisse diminuer les performances serait sans doute impopulaire. Je suis donc prêt à concéder que c'est une solution qui est loin d'être idéale - en fait, je l'ai laissé sur cette page pour des raisons historiques (je n'aime pas l'idée d'être un révisionniste).

La seconde proposition est cependant toujours importante - à la fois pour permettre aux classes qui ont un constructeur statique d'améliorer leurs performances avec la sémantique BeforeFieldInit, si elle est appropriée et pour permettre aux classes qui ont simplement besoin d'un constructeur statique, de se débarrasser de cette sémantique BeforeFieldInit pour atteindre cet objectif d'une manière mieux documentée (un développeur junior est plus susceptible de retirer un constructeur statique qui semble être inutile plutôt que de retirer un attribut qu'il ne comprend pas complètement).

Remerciements

Je tiens à remercier Jon Skeet pour son aimable autorisation de traduire cet article, ainsi que jacques_jean pour sa relecture attentive et ses corrections.