Portée de la valeur d'un champ static
Un champ static est un champ qui appartient à un type, et non pas à une instance de ce type, sa valeur est unique. Mais unique dans quelles limites ?
Contrairement à ce qu'on peut penser au premier abord, cette valeur n'est pas unique pour le processus complet. Pas plus qu'elle ne l'est, dans le cas d'une application web, pour une session.
Par défaut, en .NET, la portée de la valeur d'un champ static est le domaine d'application dans lequel l'assembly contenant le type a été chargé.
Ramené au cas de l'application web, cette valeur sera donc partagée par toutes les sessions de l'application, IIS créant un domaine d'application par répertoire virtuel.
Il existe cependant la possibilité de définir 3 autres portées :
- Par processus (RVA), bien qu'à ma connaissance nous ne puissions le faire en C# ou VB.NET, par contre c'est visiblement réalisable en IL ou C++.
Ceci dit, cette portée est limitée au types scalaires et valeur, les types référence sont exclus.
- Par thread, par domaine d'application : l'attribut ThreadStatic permet de rendre la valeur unique par thread, et toujours par domaine d'application.
- Par contexte, par domaine d'application : l'attribut ContextStatic permet de rendre la valeur unique par contexte, et toujours par domaine d'application.
Si vous venez à utiliser les attributs ThreadStatic ou ContextStatic, ne perdez pas de vue que le constructeur de classe n'est exécuté qu'une seule fois.
Ainsi, si vous fournissez une valeur initiale ou initialisez le champ via un constructeur de classe (ce qui revient au même une fois le code compilé), seul le premier accès déclenchera l'initialisation : en dehors du thread ayant accédé en premier au type, le champ marqué ThreadStatic possèdera sa valeur par défaut pour un type valeur, ou null pour un type référence. Vous devrez donc vous orienter sur une initialisation au premier accès, un booléen static pouvant vous servir de " témoin ", au lieu de se baser sur la valeur par défaut car après tout il peut s'agir d'une valeur à part entière.
Prenons comme exemple les 3 classes suivantes :
/// <summary>
/// Cette classe expose un membre static "simple".
/// </summary>
public class ClassA
{
static ClassA()
{
// Initialisation du champ via le constructeur de classe
// Ecrire
// private static Guid _ID = System.Guid.NewGuid();
// aurais eu le même effet.
ClassA._ID = System.Guid.NewGuid();
}
private static Guid _ID;
public static Guid ID
{
get
{
return ClassA._ID;
}
}
}
/// <summary>
/// Cette classe expose un membre static marqué ThreadStatic,
/// que nous initialisons dans le constructeur de classe.
/// A ne pas reproduire donc.
/// </summary>
public class ClassB
{
static ClassB()
{
// Initialisation du champ via le constructeur de classe
// Ecrire
// private static Guid _ID = System.Guid.NewGuid();
// aurais eu le même effet.
ClassB._ID = System.Guid.NewGuid();
}
[ThreadStatic()]
private static Guid _ID;
public static Guid ID
{
get
{
return ClassB._ID;
}
}
}
/// <summary>
/// Cette classe expose un membre static marqué ThreadStatic,
/// que nous initialisons la première demande, en préférant
/// ici l'utilisation d'un booléen à celle du test de la
/// valeur par défaut.
/// </summary>
public class ClassC
{
[ThreadStatic()]
private static Boolean _initialized;
[ThreadStatic()]
private static Guid _ID;
public static Guid ID
{
get
{
// si notre ID n'a pas encore été initialisé,
// nous le faisons.
if ( !ClassC._initialized )
{
ClassC._ID = System.Guid.NewGuid();
ClassC._initialized = true;
}
return ClassC._ID;
}
}
}
Nous obtenons la sortie suivante :
Thread 1
ClassA : 15382d99-b4c5-4af3-856b-643aed3f8dd7
ClassB : 78772e37-e9db-4582-85ae-32b99335d4d3
ClassC : 859cfdb3-8726-4ce5-8ad4-52413b515b97
Thread 2
ClassA : 15382d99-b4c5-4af3-856b-643aed3f8dd7
ClassB : 00000000-0000-0000-0000-000000000000
ClassC : cddee80f-b9ac-466f-a026-cba2cd01e5e2
Nous pouvons constater que pour la classe ClassA, comme attendu, la même valeur est bien présente.
En revanche pour la classe ClassB, nous pouvons nous percevoir que le champ n’est initialisé que pour le premier Thread appelant, la classe ClassC apportant quant à elle le résultat attendu.
En effectuant le test avec chargement de l'assembly dans deux domaines d'application distincts, nous pouvons bien observer des valeurs différentes suivant l'AppDomain pour l'ID de ClassA :
AppDomain 1 - Thread 1
ClassA : 50252b92-cd87-4876-9e87-9ad4d672da53
ClassB : 9e7e9976-a7c4-4fd7-98ad-9e7a4f421d72
ClassC : 45f1ee06-ca47-4423-a96f-79305fc89225
AppDomain 1 - Thread 2
ClassA : 50252b92-cd87-4876-9e87-9ad4d672da53
ClassB : 00000000-0000-0000-0000-000000000000
ClassC : dac01dc2-0d66-44c8-ad49-fb08204ba8e0
AppDomain 2 - Thread 1
ClassA : 920dd539-b2c9-489d-a33c-b736992cbc98
ClassB : b2dd2878-dc78-4910-b2f1-e0e371884608
ClassC : 5052f3fd-69e4-4aa2-8849-df42f6266f13
AppDomain 2 - Thread 2
ClassA : 920dd539-b2c9-489d-a33c-b736992cbc98
ClassB : 00000000-0000-0000-0000-000000000000
ClassC : 2201f34b-f4d1-4a23-8058-7c18171130d0
Vous pouvez trouver la solution utilisée pour les tests ici.
Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :