Bonnes pratiques de programmation avec C# par Patrick Smacchia (patrick@smacchia.com)

Ne pas inclure un même module contenant des types dans  deux assemblages différents 

Une instance d’une classe peut accéder aux membres privés d’une autre instance de cette même classe 

Ne créer pas de références croisées avec des  initialiseurs.

Eviter les appels aux méthodes virtuelles, au sein de constructeurs de types non finalisés (non-sealed)

Déclarer vos opérateurs de transtypage avec le mot clé C# explicit

N’implémenter pas d’interfaces sur vos structures.

Boxing et optimisation.

Bien que de type référence, les chaînes de caractères se comparent selon l’égalité.

Passer en référence une instance de type référence a un sens.

Comment le compilateur C# traduit les mots clé lock, delegate et la syntaxe de tableau ? 

Obliger vos clients à utiliser vos interfaces plutôt que vos implémentations.

Comment utiliser explicitement dans son code un type défini dans un assemblage chargé explicitement (i.e non référencé à la compilation) ?

 

Ne pas inclure un même module contenant des types dans  deux assemblages différents

En effet, si les deux assemblages se retrouvent chargés dans le même domaine d’application, le CLR considérera qu’il y a deux modules différents. En plus d’une perte de performance due au double chargement du même module, votre domaine d’application contiendra des types avec des noms similaires. De plus, les champs statiques seront eux aussi dupliqués.

 

Aussi, plutôt que d’inclure un même module contenant des types dans deux assemblages différents, nous vous conseillons de créer un assemblage bibliothèque (.dll) contenant ces types. Cet assemblage bibliothèque doit, bien entendu, être référencé à la compilation par les deux assemblages qui consomment ces types.

 

Une instance d’une classe peut accéder aux membres privés d’une autre instance de cette même classe

Puisque les membres privés d’une classe A ne sont accessibles qu’au sein de la classe, cela signifie qu’une instance I1 de la classe A peut accéder aux membres privés d’une instance I2 de la classe A. Soyez conscient de cette possibilité. En effet ceci n’a rien de logique et découle d’un choix arbitraire fait lors de la spécification du langage C#. Ce choix, qui est aussi appliqué en C++ et en Java, n’est pas forcément le même dans d’autres  langages orientés objet (par exemple dans SmallTalk).

 

Ne créer pas de références croisées avec des  initialiseurs

Les champs peuvent être initialisés directement durant leurs déclarations au sein de la classe. L’initialisation des champs (statiques ou non) est un sujet subtil. En effet, lorsque la classe est créée, les champs statiques sont d’abord initialisés à leurs valeurs par défaut. Puis ils sont initialisé avec leur valeur d’initialisation (pour ceux qui en ont une), dans leur ordre d’apparition dans la classe. Enfin le constructeur statique (voir plus loin) est appelé. Il en est de même pour les champs non statiques et les constructeurs non statiques. Tout ceci permet de faire des références croisées comme ci-dessous, et cette pratique est fortement déconseillée :

 

using System;

 

public class Prog

{

static int a = b+2;  // a référence b pour son initialisation

static int b = a+3;  // b référence a pour son initialisation

public static void Main()

{

       Console.WriteLine("a = {0}",a);  // a = 2

       Console.WriteLine("b = {0}",b);  // b = 5

}

}

 

Le cas de figure suivant porte aussi à confusion, d’ailleurs le résultat est différent du précédent. En effet, la construction de la classe CB se fait au milieu de la construction de la classe CA, puisque CA est appelée en premier mais CA utilise CB !

 

using System;

 

public class  CA

{

static public int a = CB.b+2;

}

public class CB

{

static public int b = CA.a+3;

}

public class Prog

{

public static void Main()

{

       Console.WriteLine("CA.a = {0}",CA.a);  // CA.a = 5

       Console.WriteLine("CB.b = {0}",CB.b);  // CB.b = 3

}

}

 

Eviter les appels aux méthodes virtuelles, au sein de constructeurs de types non finalisés (non-sealed)

En effet, contrairement au C++ et à l’instar de Java, en C# l’objet garde toujours le même type lors de la construction. De plus, les initialiseurs des champs de la classe dérivée sont appelés avant les initialiseurs des champs de la classe de base. Au contraire, le corps du constructeur de la classe de base est exécuté avant le corps du constructeur de la classe dérivée. Ainsi, un appel vers une méthode virtuelle dans le constructeur de la classe de base est dispatché vers une méthode de la classe dérivée. Une méthode de la classe dérivée est alors appelée avant le constructeur de cette même classe, et ceci est clairement à proscrire pour des raisons d’initialisation et de lisibilité du code.

 

Tout ceci est exposé par l’exemple suivant :

 

using System;

 

class CBase

{

int a = aInitialiseur();

protected CBase()

{

       Console.WriteLine("CBase.CBase()");

       f();

}

public virtual void f()

{

       Console.WriteLine("CBase.f()");

}

static int aInitialiseur()

{

       Console.WriteLine("CBase.aInitialiseur()");

       return 1;

}

}

 

class CDérivée : CBase

{

int b = bInitialiseur();

int i = 0;

public CDérivée()

{    

       i = 1;

       Console.WriteLine("CDérivée.CDérivée() i vaut {0}",i);

}

public override void f()

{

       Console.WriteLine("CDérivée.f() i vaut {0}",i);

}

static int bInitialiseur()

{

       Console.WriteLine("CDérivée.bInitialiseur()");

       return 2;

}

}

 

class Prog

{

public static void Main()

{

       CDérivée Foo = new CDérivée();

}

}

 

Ce programme affiche :

 

CDérivée.bInitialiseur()

CBase.aInitialiseur()

CBase.CBase()

CDérivée.f() i vaut 0

CDérivée.CDérivée() i vaut 1

 

Déclarer vos opérateurs de transtypage avec le mot clé C# explicit

Il est toujours préférable de déclarer ses opérateurs de transtypage comme explicites. Dans le cas contraire votre code risque d’être permissif, c’est-à-dire que le compilateur aura à faire des choix. Au mieux il produira une erreur de compilation car il ne saura pas quel choix faire. Au pire, il ne donnera pas d’avertissements et fera le choix contraire à votre attente. Dans le programme suivant, une instance de la classe Distance peut être convertie implicitement en une instance du type  double ou une instance de la classe string. Quel choix de transtypage le compilateur doit-il faire lorsque l’on veut afficher cet objet avec la méthode Console.WriteLine(object) ?

 

using System;

 

public class Distance

{

public double m_Mesure = 0.0;

public Distance(double d){ m_Mesure = d; }

public static implicit operator double(Distance D)

{

       return D.m_Mesure;

}

public static implicit operator string(Distance D)

{

       return string.Format("Distance:{0:##.##} mètres",D.m_Mesure);

}

}

 

class Prog

{

static void Main(string[] args)

{

       Distance D1 = new Distance(5.3);

       // Erreur de compilation: Opérateur de transtypage ambiguë.

       // Doit-on transtyper D1 en un double ou un string ?

       Console.WriteLine(D1);

       // OK pas d'ambiguïté, mais il vaut mieux déclarer les

       // opérateurs de transtypage comme explicite.

       Console.WriteLine((string)D1);

}

}

  

N’implémenter pas d’interfaces sur vos structures

Voici un problème classique qui survient lorsqu’une structure implémente une interface. Si vous essayez d’accéder aux membres de l’interface implémentés par la structure à partir d’une référence vers l’interface, vous n’obtiendrez certainement pas le résultat escompté. En effet, lors du transtypage de la structure vers l’interface, une opération de boxing est implicitement réalisée car l’interface à besoin d’une référence. Pour vous en convaincre, voyez l’exemple suivant :

 

using System;

 

interface IInterface

{

void SetState(int i);

int  GetState();

}

 

struct Struct : IInterface

{

private int i;

public void SetState(int i)

{

       this.i = i;

}

public int GetState()

{

       return i;

}

}

 

class Prog

{

static void Main(string[] args)

{

       Struct S = new Struct();

       // Ici un boxing de la structure est réalisé implicitement.

       IInterface I = (IInterface) S;

       S.SetState(10);

       I.SetState(20);

       Console.WriteLine("Retour de S.GetState():"+S.GetState());

       Console.WriteLine("Retour de I.GetState():"+I.GetState());

}

}

 

Ce programme affiche :

Retour de S.GetState():10

Retour de I.GetState():20

 

En règle générale il vaut mieux ne pas implémenter d’interface dans les structures. Si vous n’avez pas d’autres possibilités, il vaut mieux appeler les méthodes de l’interface à partir de l’implémentation, contrairement à ce qui est conseillé pour les classes. Pour les plus sceptiques, l’analyse avec ildasm.exe du code CIL généré pour la méthode Main() montre clairement qu’il y a une opération de boxing réalisée. Notez que les deux autres opérations de boxing sont nécessaires pour pouvoir afficher l’état l’entier retourné par la méthode GetState():

 

.method private hidebysig static void  Main(string[] args) cil managed

{

  .entrypoint

  // Code size       86 (0x56)

  .maxstack  2

  .locals ([0] valuetype Struct S,

           [1] class IInterface I)

  IL_0000:  ldloca.s   S

  IL_0002:  initobj    Struct

  IL_0008:  ldloc.0

  IL_0009:  box        Struct  // <- c’est ici que l’opération de boxing

                               //    de la structure a implicitement lieu.

  IL_000e:  stloc.1

  IL_000f:  ldloca.s   S

  IL_0011:  ldc.i4.s   10

  IL_0013:  call       instance void Struct::SetState(int32)

  IL_0018:  ldloc.1

  IL_0019:  ldc.i4.s   20

  IL_001b:  callvirt   instance void IInterface::SetState(int32)

  IL_0020:  ldstr      "Retour de S.GetState():"

  IL_0025:  ldloca.s   S

  IL_0027:  call       instance int32 Struct::GetState()

  IL_002c:  box        [mscorlib]System.Int32

  IL_0031:  call       string [mscorlib]System.String::Concat(object,object)

  IL_0036:  call       void [mscorlib]System.Console::WriteLine(string)

  IL_003b:  ldstr      "Retour de I.GetState():"

  IL_0040:  ldloc.1

  IL_0041:  callvirt   instance int32 IInterface::GetState()

  IL_0046:  box        [mscorlib]System.Int32

  IL_004b:  call       string [mscorlib]System.String::Concat(object,object)

  IL_0050:  call       void [mscorlib]System.Console::WriteLine(string)

  IL_0055:  ret

} // end of method Prog::Main

 

Boxing et optimisation

L’opération de boxing a un coût non négligeable, aussi il peut être tentant d’essayer de l’optimiser. Par exemple, le premier des deux programmes suivants n’utilise qu’une seule fois le boxing sur la variable i alors que le second boxe la variable i deux fois. Cependant, ces programmes ne sont pas équivalents (le premier affiche "Références différentes" alors que le second affiche "Même références").

 

using System;

 

class Prog

{

static void f(object o1,object o2)

{

       if(o1==o2)

             Console.WriteLine("Même références");

       else

             Console.WriteLine("Références différentes");

}

public static void Main( String[] argv )

{

       int i = 9 ;

       f(i,i); /* On a deux boxing de i donc deux références différents vers i */

}

}

 

using System;

 

class Prog

{

static void f(object o1,object o2)

{

       if(o1==o2)

             Console.WriteLine("Même références");

       else

             Console.WriteLine("Références différentes");

}

public static void Main( String[] argv )

{

       int i = 9 ;

      object o = i;

       f(o,o); /* pas de boxing car o est déjà une référence */

}

}

 

Il vaut donc mieux ne pas essayer d’optimiser l’utilisation du boxing. Cependant, il existe un cas précis où l’optimisation du boxing peut se révéler très payante. Lors du parcours des éléments d’un dictionnaire avec la syntaxe foreach, une instance de la structure DictionaryEntry est unboxée lors de chaque itération. Pour vous en convaincre, observez le programme suivant ainsi que son analyse par ildasm.exe.

 

using System;

using System.Collections;

 

class Prog

{

public static void Main()

{

       // Capacité initiale de 100.

       Hashtable Tbl = new Hashtable(100);

       Tbl.Add("France","20 Rue Arson");

       Tbl.Add("Francis","90 Rue Barberis");

       foreach( DictionaryEntry DE in Tbl)

             Console.WriteLine(DE.Key + " : " + DE.Value );

}

}

 

...

  IL_0031:  ldloc.2

  IL_0032:  callvirt   instance object

               [mscorlib]System.Collections.IEnumerator::get_Current()

  IL_0037:  unbox      [mscorlib]System.Collections.DictionaryEntry

  IL_003c:  ldobj      [mscorlib]System.Collections.DictionaryEntry

...

 

Le code suivant, strictement équivalent, rend les opérations de unboxing pour chaque structure DictionaryEntry, plus évidentes :

 

...

foreach( Object obj in Tbl)

{

       // La ligne suivante implique une opération de unboxing

       DictionaryEntry DE = (DictionaryEntry) obj;

       Console.WriteLine(DE.Key + " : " + DE.Value );

}

...

 

Pour éviter ces opérations de boxing sur la structure DictionaryEntry, il faut utiliser le code suivant. Il est moins convivial, puisqu’il n’utilise pas les mots-clés foreach et in, mais il est plus performant :

 

...

IDictionaryEnumerator E = Tbl.GetEnumerator();

while( E.MoveNext() )

       Console.WriteLine(E.Key + " : " + E.Value );

...

 

Bien que de type référence, les chaînes de caractères se comparent selon l’égalité 

Cela vaut la peine d’être noté, dans la mesure où la classe string est de type référence.

 

class Prog

{

public static void Main()

{

       string s1 = "hello";

       string s2 = "hel";

       string s3 = "lo";

       string s4 = s2+s3;

       bool b = (s1 == s4);

       // Ici, bien que s1 et s4 référencent à l'évidence

       // deux objets différents, b vaut true.