|
|
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
Déclarer vos opérateurs de transtypage avec le mot clé C# explicit
N’implémenter pas d’interfaces sur vos structures
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.
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.
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).
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
}
}
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
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);
}
}
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
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 );
...
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.