Aller au menu - Aller au contenu

Icône Plus loin avec le C# et .NET

Mise à jour : 02/02/2012
Difficulté : Intermédiaire Intermédiaire Creative Commons BY-NC-SA
22 955 visites depuis 7 jours, dont 173 sur ce chapitre classé 15/786
Maintenant que nous en savons plus, nous allons pouvoir aborder quelques notions qui me paraissent importantes et que nous n'avons pas encore traitées. Ce sera l’occasion d’approfondir nos connaissances et de comprendre un peu mieux certains points qui auraient pu vous paraître obscurs.

Nous allons voir des instructions C# supplémentaires qui vont nous permettre de compléter nos connaissances en POO. Nous en profiterons également pour détailler comment le framework .NET gère les types en mémoire. Vous en saurez plus sur le formatage des chaînes et aurez un aperçu du côté obscur du framework .NET, la réflexion !

Bref, tout plein de nouvelles cordes à nos arcs nous permettant d'être de plus en plus efficace avec le C#. Ces nouveaux concepts vous serviront sans doute moins souvent, mais sont importants à connaître.
Sommaire du chapitre :
Icône du chapitre
Chapitre précédent Sommaire Chapitre suivant

Empêcher une classe de pouvoir être héritée

Parmi ces nouveaux concepts, nous allons voir comment il est possible de faire en sorte qu'une classe ne puisse pas être héritée. Rappelez-vous, à un moment j’ai dit qu’on ne pouvait pas créer une classe qui spécialise la classe String...

C’est quoi ce mystère ? Nous, quand nous créons une classe, n’importe qui peut en hériter. Pourquoi pas la classe String ?


C’est parce que je vous avais caché le mot-clé sealed. Il permet d’empêcher la classe d’être héritée. Tout simplement.

Pourquoi vouloir empêcher d’étendre une classe en la dérivant ?
Pour plusieurs raisons, mais généralement nous allons recourir à ce mot-clé lorsqu’une classe est trop spécifique à une méthode ou à une bibliothèque et que l’on souhaite empêcher quelqu’un de pouvoir obtenir du code instable en étendant son fonctionnement. C’est typiquement le cas pour la classe String. Il y a beaucoup de classes dans le framework .NET qui sont marquées comme sealed afin d’éviter que l’on puisse faire n’importe quoi.

Il faut par contre faire attention car l’emploi de ce mot-clé restreint énormément la façon dont on pourrait employer cette classe.
Pour déclarer une classe sealed (scellée en français) il suffit de précéder le mot-clé class du mot-clé sealed :

Code : C#
1
2
3
public sealed class ClasseImpossibleADeriver
{
}


Ainsi, toute tentative d’héritage :

Code : C#
1
2
3
public class MaClasse : ClasseImpossibleADeriver
{
}


se soldera par une erreur de compilation déshonorante :

Code : Console
impossible de dériver du type sealed 'MaPremiereApplication.ClasseImpossibleADeriver'


À noter que le mot-clé sealed fonctionne également pour les méthodes, dans ce cas, cela permet d’empêcher la substitution d’une méthode. Reprenons notre exemple du chien muet :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class Chien
{
    public virtual void Aboyer()
    {
        Console.WriteLine("Wouf");
    }
}

public class ChienMuet : Chien
{
    public sealed override void Aboyer()
    {
        Console.WriteLine("...");
    }
}


Ici, nous marquons la méthode comme sealed, ce qui fait qu’une classe qui dérive de ChienMuet ne sera pas capable de remplacer la méthode permettant d’aboyer. Le code suivant :

Code : C#
1
2
3
4
5
6
7
public class ChienAvecSyntheseVocale : ChienMuet
{
    public override void Aboyer()
    {
        Console.WriteLine("Bwarf");
    }
}


entrainera l’erreur de compilation :

Code : Console
'MaPremiereApplication.ChienAvecSyntheseVocale.Aboyer()' : ne peut pas se substituer à un membre hérité 'MaPremiereApplication.ChienMuet.Aboyer()', car il est sealed


Voilà pour ce mot-clé, à utiliser en connaissance de cause.

Précisions sur les types et gestion mémoire

Nous avons vu beaucoup de types différents au cours des chapitres précédents. Nous avons vu les structures, les classes, les énumérations, les délégués, etc.
Certains sont des types valeur et d’autres des types référence. Et ils sont tous des objets.

Voici un petit schéma récapitulatif des types que nous avons déjà vus :

Image utilisateur


Nous avons dit brièvement qu’ils étaient gérés différemment par le framework .NET et que les variables de type valeur contenaient directement la valeur, alors que les variables de type référence possèdent un lien vers un objet en mémoire.
Ce qu’il se passe en fait quand nous déclarons une variable de type valeur, c’est qu’il crée directement la valeur en mémoire dans ce qu’on appelle « la pile ». C’est une zone mémoire (limitée) où il est très rapide de mettre et de chercher des valeurs. De plus, cette mémoire n’a pas besoin d’être libérée par le ramasse-miettes (que nous verrons un peu plus bas). C’est pour cela que pour des raisons de performances on peut être amené à choisir les types valeur.
Par exemple lorsque nous faisons :

Code : C#
1
2
int age = 10;
double pourcentage = 5.5;


il se passe ceci en mémoire :

Image utilisateur


La variable age est une adresse mémoire contenant la valeur 10. C’est la même chose pour la variable pourcentage sauf que l’emplacement mémoire est plus important car on peut stocker plus de choses dans un double que dans un int.
En ce qui concerne les types valeur, lorsque nous instancions par exemple une classe, le C# instancie la variable maVoiture dans la pile et lui met dedans une référence vers le vrai objet qui lui est alloué sur une zone mémoire de capacité plus importante, mais à accès plus lent, que l’on appelle « le tas ». Comme il est géré par le framework .NET, on l’appelle « le tas managé ».

Si nous avons le code suivant :

Code : C#
1
2
3
4
5
6
7
public class Voiture
{
    public int Vitesse { get; set; }
    public string Marque { get; set; }
}

Voiture maVoiture = new Voiture { Vitesse = 10, Marque = "Peugeot" };


Nous aurons en mémoire :

Image utilisateur


À noter que lorsqu’une variable sort de sa portée et qu’elle n’est plus utilisable par qui que ce soit, alors la case mémoire sur la pile est marquée comme de nouveau libre.
Voilà grosso modo comment est gérée la mémoire.

Il manque encore un élément fondamental du mécanisme de gestion mémoire : le ramasse-miettes.
Nous en avons déjà parlé brièvement, le ramasse miette sert à libérer la mémoire qui n’est plus utilisée dans le tas managé. Il y aurait de quoi écrire un gros tutoriel rien que sur son fonctionnement, aussi nous allons expliquer rapidement comment il fonctionne sans trop rentrer dans les détails.

Le ramasse-miettes est souvent dénommé par sa traduction anglaise, le garbage collector.


Pourquoi libérer la mémoire ?
On a vu que quand une variable sort de portée, alors la case mémoire sur la pile est marquée comme à nouveau libre. C’est très bien avec les types valeur qui sont uniquement sur la pile. Mais avec les types référence ?
Que donne le code suivant lorsque nous quittons la méthode CreerVoiture ?

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class Voiture
{
    public int Vitesse { get; set; }
    public string Marque { get; set; }
}

static void Main(string[] args)
{
    CreerVoiture();
}

public static void CreerVoiture()
{
    Voiture maVoiture = new Voiture { Vitesse = 10, Marque = "Peugeot" };
}


Nous avons dit que la mémoire sur la pile redevenait libre. La référence vers l’objet est donc cassée. Cependant, la mémoire sur le tas est toujours occupée.

Image utilisateur


Cela veut dire que notre objet est toujours en mémoire sauf que la variable maVoiture n’existe plus et donc, ne référence plus cet objet.
Dans un langage comme le C++, nous aurions été obligés de vider explicitement la mémoire référencée par la variable.
En C#, pas besoin ! C’est le ramasse-miettes qui le fait pour nous. Encore un truc de fainéant ça. ^^

En fait, c’est plutôt une sécurité qu’un truc de fainéant. C’est la garantie que la mémoire utilisée par les objets qui n’existent plus est vidée et, du coup, redevient disponible pour de nouveaux objets. Grâce au ramasse-miettes, nous évitons ce que l’on appelle les fuites mémoires.

Super génial ça. Mais, il passe quand ce ramasse-miettes ?


La réponse est simple : « on ne sait pas quand il passe ».
En fait, il passe quand il a besoin de mémoire ou quand l’application semble ne rien faire. Évidemment, lorsque le ramasse-miettes passe, les performances de l’application sont un petit peu pénalisées. C’est pour cela que l’algorithme de passage du ramasse-miettes est optimisé pour essayer de déranger le moins possible l’application, lors des moments d’inactivité par exemple. Par contre, quand il y a besoin de mémoire, il ne se pose pas la question : il passe quand même.

Il y aurait beaucoup de choses à dire encore sur ce mécanisme mais arrêtons-nous là.
Retenons que le ramasse-miettes est un superbe outil qui nous permet de garantir l’intégrité de notre mémoire et qu’il s’efforce au mieux de nous embêter le moins possible. Il est important de libérer les ressources dont on n'a plus besoin quand c’est possible, par exemple en se désabonnant d’un événement ou lorsque nous avons utilisé des ressources natives (ce que nous ne verrons pas dans ce tutoriel) ou lors d’accès à des fichiers ou des bases de données.

Masquer une méthode

Dans la partie précédente sur la substitution, nous avons vu qu'on pouvait substituer une méthode grâce au mot-clé override. Cette méthode devait d'ailleurs s'être déclarée comme candidate à cette substitution grâce au mot-clé virtual.
Rappelez-vous de ce code :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class Chien
{
    public virtual void Aboyer()
    {
        Console.WriteLine("Wouaf !");
    }
}

public class Caniche : Chien
{
    public override void Aboyer()
    {
        Console.WriteLine("Wiiff");
    }
}


Si nous instancions des chiens et des caniches de cette façon :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
static void Main(string[] args)
{
    Chien chien = new Chien();
    Caniche caniche = new Caniche();
    Chien canicheTraiteCommeUnChien = new Caniche();

    chien.Aboyer();
    caniche.Aboyer();
    canicheTraiteCommeUnChien.Aboyer();
}


Nous aurons :

Code : Console
Wouaf !
Wiiff
Wiiff


En effet, le premier objet est un chien et les deux suivants sont des caniches. Grâce à la substitution, nous faisons aboyer notre chien, notre caniche et notre caniche qui se fait manipuler comme un chien.

Il est possible de masquer des méthodes qui ne sont pas forcément marquées comme virtuelles grâce au mot-clé new.
À ce moment-là, la méthode ne se substitue pas à la méthode dérivée mais la masque pour les types dérivés. Voyons cet exemple :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class Chien
{
    public void Aboyer()
    {
        Console.WriteLine("Wouaf !");
    }
}

public class Caniche : Chien
{
    public new void Aboyer()
    {
        Console.WriteLine("Wiiff");
    }
}


Remarquons que la méthode Aboyer() de la classe Chien n’est pas marquée virtual et que la méthode Aboyer de la classe Caniche est marquée avec le mot-clé new. Il ne s’agit pas ici de substitution. Il s’agit juste de deux méthodes qui portent le même nom, mais la méthode Aboyer() de la classe Caniche se charge de masquer la méthode Aboyer() de la classe mère.
Ce qui fait que les précédentes instanciations :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
static void Main(string[] args)
{
    Chien chien = new Chien();
    Caniche caniche = new Caniche();
    Chien canicheTraiteCommeUnChien = new Caniche();

    chien.Aboyer();
    caniche.Aboyer();
    canicheTraiteCommeUnChien.Aboyer();
}


produiront cette fois-ci :

Code : Console
Wouaf !
Wiiff
Wouaf !


C’est le dernier cas qui peut surprendre. Rappelez-vous, il ne s’agit pas d’un remplacement de méthode cette fois-ci, et donc le fait de manipuler le caniche en tant que Chien ne permet pas de remplacer le comportement de la méthode Aboyer(). Dans ce cas, on appelle bien la méthode Aboyer correspondant au type Chien, ce qui a pour effet d’afficher « Wouaf ! ».
Si nous n’avions pas utilisé le mot-clé new, nous aurions eu le même comportement mais le compilateur nous aurait avertis de ceci grâce à un avertissement :

Code : Console
'MaPremiereApplication.Caniche.Aboyer()' masque le membre hérité 'MaPremiereApplication.Chien.Aboyer()'. Utilisez le mot clé new si le masquage est intentionnel.</citation>


Il nous précise que nous masquons effectivement la méthode de la classe mère et que si c’est fait exprès, alors il faut l’indiquer grâce au mot-clé new afin de lui montrer que nous savons ce que nous faisons.
Notez quand même que dans la majorité des cas, en POO, ce que vous voudrez faire c’est bien substituer la méthode et non la masquer.

C’est le même principe pour les variables, il est également possible de masquer une variable, quoiqu’en général, cela reflète plutôt une erreur dans le code. Si nous faisons :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class Chien
{
    protected int age;
}

public class Caniche : Chien
{
    private int age;

    public Caniche()
    {
        age = 0;
    }

    public override string ToString()
    {
        return "J'ai " + age + " ans";
    }
}


Alors le compilateur nous indique que la variable age de la classe Caniche masque celle dont nous avons hérité de la classe Chien. Si c’est intentionnel, il nous propose de la marquer avec le mot-clé new. Sinon, cela nous permet de constater que cette variable est peut-être inutile… (sûrement d’ailleurs !)

Le mot-clé yield

Nous avons vu dans le TP sur les génériques comment créer un énumérateur. C’est une tâche un peu lourde qui nécessite pas mal de code.
Omettons un instant que la classe String soit énumérable et créons pour l’exemple une classe qui permette d’énumérer une chaine de caractères. Nous aurions pu faire quelque chose comme ça :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class ChaineEnumerable : IEnumerable<char>
{
    private string chaine;
    public ChaineEnumerable(string valeur)
    {
        chaine = valeur;
    }

    public IEnumerator<char> GetEnumerator()
    {
        return new ChaineEnumerateur(chaine);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return new ChaineEnumerateur(chaine);
    }
}

public class ChaineEnumerateur : IEnumerator<char>
{
    private string chaine;
    private int indice;

    public ChaineEnumerateur(string valeur)
    {
        indice = -1;
        chaine = valeur;
    }

    public char Current
    {
        get { return chaine[indice]; }
    }

    public void Dispose()
    {
    }

    object IEnumerator.Current
    {
        get { return Current; }
    }

    public bool MoveNext()
    {
        indice++;
        return indice < chaine.Length;
    }

    public void Reset()
    {
        indice = -1;
    }
}


Nous avons une classe ChaineEnumerable qui encapsule une chaine de caractères de type string. Et une classe ChaineEnumerateur qui permet de parcourir une chaine et de renvoyer à chaque itération un caractère, représenté par le type char (qui est un alias de la structure System.Char).

Rien de bien sorcier, mais un code plutôt lourd.

Regardons à présent le code suivant :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class ChaineEnumerable : IEnumerable<char>
{
    private string chaine;
    public ChaineEnumerable(string valeur)
    {
        chaine = valeur;
    }

    public IEnumerator<char> GetEnumerator()
    {
        for (int i = 0; i < chaine.Length; i++)
        {
            yield return chaine[i];
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}


Il fait la même chose, mais d’une manière bien simplifiée. Nous remarquons l’apparition du mot-clé yield. Il permet de créer facilement des énumérateurs. Il permet, combiné au mot-clé return, de renvoyer un élément d’une collection et de passer à l’élément suivant.
Il peut être combiné au mot-clé break également pour permettre d’arrêter l’énumération. Ce qui veut dire que nous aurions pu écrire la méthode GetEnumerator() également de cette façon :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public IEnumerator<char> GetEnumerator()
{
    int i = 0;
    while (true)
    {
        if (i == chaine.Length)
            yield break;
        yield return chaine[i];
        i++;
    }
}


Ce qui est plus laid, je le conçois :p , mais qui permet d’illustrer le mot-clé yield break.

Le mot-clé yield permet également de renvoyer un IEnumerable (ou sa version générique), ainsi un peu bêtement, on pourrait faire :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
static void Main(string[] args)
{
    foreach (string prenom in ObtenirListeDePrenoms())
    {
        Console.WriteLine(prenom);
    }
}

public static IEnumerable<string> ObtenirListeDePrenoms()
{
    yield return "Nicolas";
    yield return "Jérémie";
    yield return "Delphine";
}


Cet exemple va surtout me permettre d’illustrer ce qu’il se passe exactement grâce au mode debug.
Mettez un point d’arrêt au début du programme et lancez-le en mode debug :

Image utilisateur


Appuyez sur F11 plusieurs fois pour rentrer dans la méthode ObtenirListeDePrenoms, nous voyons l’indicateur se déplacer sur les différentes parties de l’instruction foreach. Puis nous arrivons sur la première instruction yield return :

Image utilisateur


Appuyons à nouveau sur F11 et nous pouvons constater que nous sortons de la méthode et que nous rentrons à nouveau dans le corps de la boucle foreach qui va nous afficher le premier prénom. Continuons les F11 jusqu’à repasser sur le mot-clé in et rentrer à nouveau dans la méthode ObtenirListeDePrenoms() et nous pouvons constater que nous retournons directement au deuxième mot-clé yield :

Image utilisateur


Et ainsi de suite jusqu’à ce qu’il n’y ait plus rien dans la méthode ObtenirListeDePrenoms() et que du coup le foreach se termine.
À la première lecture, ce n’est pas vraiment ce comportement que nous aurions attendu…

Voilà pour le principe du yield return.
À noter qu’un yield break nous aurait fait sortir directement de la boucle foreach.

Le mot-clé yield participe à ce qu’on appelle l’exécution différée. On évalue la méthode ObtenirListeDePrenoms() qu’au moment où on parcoure le résultat avec la boucle foreach, ce qui pourrait paraître surprenant.
Par exemple, si nous faisons :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public static IEnumerable<string> ObtenirListeDePrenoms()
{
    yield return "Nicolas";
    yield return "Jérémie";
    yield return "Delphine";
}

static void Main(string[] args)
{
    IEnumerable<string> prenoms = ObtenirListeDePrenoms();
    Console.WriteLine("On fait des choses ...");
    foreach (string prenom in prenoms)
    {
        Console.WriteLine(prenom);
    }
}


Et que nous mettons un point d’arrêt sur le Console.WriteLine et un autre point d’arrêt dans la méthode ObtenirListeDePrenoms(), alors étrangement, on va s’arrêter dans un premier temps sur le point d’arrêt positionné sur la ligne où il y a le Console.WriteLine et ensuite nous passerons dans le point d’arrêt positionné dans la méthode ObtenirListeDePrenoms().
En effet, on ne passera dans la méthode que lorsque nous itérerons explicitement sur les prénoms grâce au foreach.
Nous reviendrons sur cette exécution différée un peu plus loin. C’est le mot-clé yield qui fait ça.

Le formatage de chaines, de dates et la culture

Pour l’instant, lorsque nous avons eu besoin de mettre en forme des chaines de caractères, nous avons utilisé la méthode Console.WriteLine avec en paramètres des chaines concaténées entre elles grâce à l’opérateur +. Par exemple :

Code : C#
1
2
int age = 30;
Console.WriteLine("J'ai " + age + " ans");


Il est important de savoir qu’il est possible de formater la chaine un peu plus simplement et surtout beaucoup plus efficacement. Ceci est possible grâce à la méthode string.Format. Le code précédent peut par exemple être remplacé par :

Code : C#
1
2
3
int age = 30;
string chaine = string.Format("J'ai {0} ans", age);
Console.WriteLine(chaine);


La méthode string.Format accepte en premier paramètre un format de chaine. Ici, nous lui indiquons une chaine de caractères avec au milieu un caractère spécial : {0}. Il permet de dire : « remplace-moi le {0} avec le premier paramètre qui va suivre », en l’occurrence ici la variable age. Si nous avons plusieurs valeurs, il est possible d’utiliser les caractères spéciaux {1}, puis {2}, etc … chaque nombre représentant l’indice correspondant au paramètre. Par exemple :

Code : C#
1
2
3
4
double valeur1 = 10.5;
double valeur2 = 4.2;
string chaine = string.Format("{0} multiplié par {1} s'écrit \"{0} * {1}\" et donne comme résultat : {2}", valeur1, valeur2, valeur1 * valeur2);
Console.WriteLine(chaine);


Ce qui donne :

Code : Console
10,5 multiplié par 4,2 s'écrit "10,5 * 4,2" et donne comme résultat : 44,1


Vous avez pu remarquer qu’il est possible d’utiliser le même paramètre à plusieurs endroits.
La méthode Console.WriteLine dispose aussi de la capacité de formater des chaines de caractères. Ce qui veut dire que le code précédent peut s’écrire :

Code : C#
1
2
3
double valeur1 = 10.5;
double valeur2 = 4.2;
Console.WriteLine("{0} multiplié par {1} s'écrit \"{0} * {1}\" et donne comme résultat : {2}", valeur1, valeur2, valeur1 * valeur2);



Ce qui revient absolument au même.

Parlons culture désormais. N’ayez pas peur, rien à voir avec des questions pour des champions, la culture en informatique correspond à tout ce qui a trait aux différences entre les langues et les paramètres locaux. Par exemple, en France nous n'écrivons pas les dates de la même façon qu’aux États-Unis. En France, nous écririons le jour de Noël 2011 de cette façon :


Citation : En France
Le 25/12/2011


Alors qu’aux États-Unis, ils l’écriraient :

Citation : Aux États-Unis
The 12/25/2011


En inversant donc le mois et le jour.
De la même façon, il existe des différences lorsque nous écrivons les nombres à virgule. En France, nous écririons :

Citation : En France
123,45


Alors qu’aux États-Unis, ils l’écriraient :

Citation : Aux États-Unis
123.45


Un point à la place d’une virgule, subtile différence.
On a donc souvent des différences entre les langues. Et même plus, on peut avoir des différences entre les langues en fonction de l’endroit où elles sont parlées. Citons par exemple le français de France et le français du Canada.
Il existe donc au sein du système d’exploitation tout une liste de « régions » représentées par un couple de lettres. Par exemple, le français est représenté par les lettres fr. Et même plus précisément, le français de France est représenté par fr-FR alors que celui du Canada est représenté par fr-CA.
C’est ce que l’on retrouve dans les paramètres de notre système d’exploitation lorsqu’on va (sous Windows 7) dans le panneau de configuration, « Horloge langue et région » :

Image utilisateur


Puis « Modifier le format de la date, de l’heure ou des nombres ».

Image utilisateur


Nous pouvons adapter le format à celui que nous préférons :

Image utilisateur


Bref, ce choix, nous le retrouvons dans notre application si nous récupérons la culture courante :

Code : C#
1
Console.WriteLine(System.Threading.Thread.CurrentThread.CurrentCulture);


Avec ceci, nous aurons :

Code : Console
fr-FR


Ce qui est intéressant, c’est que le formatage des chiffres ou des dates change en fonction de la culture. Ainsi, si nous utilisons le français de France, pour le code suivant :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public static void Main(string[] args)
{
    Affiche();
}

public static void Affiche()
{
    Console.WriteLine(System.Threading.Thread.CurrentThread.CurrentCulture);
    decimal dec = 5.5M;
    double dou = 4.8;
    DateTime date = new DateTime(2011, 12, 25);
    Console.WriteLine("Décimal : {0}", dec);
    Console.WriteLine("Double : {0}", dou);
    Console.WriteLine("Date : {0}", date);
}


nous aurons :

Code : Console
fr-FR
Décimal : 5,5
Double : 4,8
Date : 25/12/2011 00:00:00


Il est possible de changer la culture courante de notre application, si par exemple nous souhaitons qu’elle soit multiculture, en utilisant le code suivant :

Code : C#
1
System.Threading.Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");


Ici par exemple, j’indique à mon application que je souhaite travailler avec la culture des États-Unis. Ainsi, le code précédent, en changeant juste la culture :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public static void Main(string[] args)
{
    System.Threading.Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
    Affiche();
}

public static void Affiche()
{
    Console.WriteLine(System.Threading.Thread.CurrentThread.CurrentCulture);
    decimal dec = 5.5M;
    double dou = 4.8;
    DateTime date = new DateTime(2011, 12, 25);
    Console.WriteLine("Décimal : {0}", dec);
    Console.WriteLine("Double : {0}", dou);
    Console.WriteLine("Date : {0}", date);
}


produira un résultat légèrement différent :

Code : Console
en-US
Décimal : 5.5
Double : 4.8
Date : 12/25/2011 12:00:00 AM


Il s’agit exactement de la même information, mais formatée différemment.

À noter qu’il existe encore une autre information de culture, à savoir la langue du système d’exploitation que l’on retrouve dans la culture de l’interface graphique, dans la propriété :

Code : C#
1
Console.WriteLine(System.Threading.Thread.CurrentThread.CurrentUICulture);


Il s’agit ici de la CurrentUICulture, à ne pas confondre avec la CurrentCulture. Correspondant à la langue du système d’exploitation, nous nous en servirons plus volontiers pour rendre une application multi-langue.

Pour finir sur le formatage des chaines, il est possible d’utiliser des caractères spéciaux enrichis pour changer le formatage des types numériques ou de la date.
Par exemple, il est possible d’afficher la date sous une forme différente avec :

Code : C#
1
2
DateTime date = new DateTime(2011, 12, 25);
Console.WriteLine("La date est {0}", date.ToString("dd-MM-yyyy"));


Ici, dd sert à afficher le jour, MM le mois et yyyy l’année sur quatre chiffre, ce qui donne :

Code : Console
La date est 25-12-2011


Il y a beaucoup d’options différentes que vous pouvez retrouver dans la documentation.

Cela fonctionne dans le même genre pour les numériques, qui nous permettent par exemple d’afficher le nombre de différentes façons. Ici par exemple, j’utilise la notation scientifique :

Code : C#
1
2
double valeur = 123.45;
Console.WriteLine("Format scientifique : {0:e}", valeur);


Ce qui donne :

Code : Console
Format scientifique : 1,234500e+002


Pour les nombres, il existe différents formatages que l’on peut retrouver dans le tableau ci-dessous :

Format Description Remarque
{0:c} Monnaie Attention, la console affiche mal le caractère €
{0:d} Décimal Ne fonctionne qu’avec les types sans virgules
{0:e} Notation scientifique
{0:f} Virgule flottante
{0:g} Général Valeur par défaut
{0:n} Nombre avec formatage pour les milliers
{0:r} Aller-retour Permet de garantir que la valeur numérique convertie en chaine pourra être convertie à nouveau en valeur numérique identique
{0:x} Héxadécimal Ne fonctionne qu’avec les types sans virgules


À noter que l’on peut également avoir du formatage grâce à la méthode ToString(). Il suffit de passer le format en paramètres, comme ici où cela me permet d’afficher uniquement le nombre de chiffres après la virgule qui m’intéressent ou des chiffres significatifs :

Code : C#
1
2
double valeur = 10.0 / 3;
Console.WriteLine("Avec 3 chiffres significatifs et 3 chiffres après la virgule : {0}", valeur.ToString("000.###"));


Qui donne :

Code : Console
Avec 3 chiffres significatifs et 3 chiffres après la virgule : 003,333


Mettre le format dans la méthode ToString() fonctionne aussi pour la date :

Code : C#
1
2
DateTime date = new DateTime(2011, 12, 25);
Console.WriteLine(date.ToString("MMMM"));


Ici, je n’affiche que le nom du mois :

Code : Console
décembre


Voilà pour le formatage des chaines de caractères.
Lister tous les formatages possibles serait une tâche bien ennuyeuse. Vous aurez l’occasion de rencontrer d’autres formatages qui permettent de faire un peu tout et n’importe quoi. Vous pourrez retrouver la plupart des formatages à partir de ce lien.

Les attributs

Dans la liste des choses qui vont vous servir, on peut rajouter les attributs.
Ils sont utilisés pour fournir des informations complémentaires sur une méthode, une classe, variables, etc. Ils vont nous servir régulièrement dans le framework .NET et nous aurons même la possibilité de créer nos propres attributs.

Un attribut est déclaré entre crochets avec le nom de sa classe. Il peut se mettre au-dessus d’une classe, d’une méthode, d’une propriété, etc.
Par exemple, dans le code ci-dessous, je positionne l’attribut Obsolete au-dessus de la déclaration d’une méthode :


Code : C#
1
2
3
4
5
[Obsolete("Utilisez plutôt la méthode ToString() pour avoir une représentation de l'objet")]
public void Affiche()
{
    // ...
}


Ici, j’utilise un attribut existant du framework .NET, l’attribut Obsolete qui permet d’indiquer qu’une méthode est obsolète et qu’il ne vaudrait mieux pas l’utiliser. En général, il est possible de fournir une information permettant de dire pourquoi la méthode est obsolète et par quoi il faut la remplacer. Ainsi, si nous tentons d’utiliser cette méthode :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class Program
{
    static void Main(string[] args)
    {
        new Program().Affiche();
    }

    [Obsolete("Utilisez plutôt la méthode ToString() pour avoir une représentation de l'objet")]
    public void Affiche()
    {
        // ...
    }
}


Le compilateur nous affichera l’avertissement suivant :

Code : Console
'MaPremiereApplication.Program.Affiche()' est obsolète : 'Utilisez plutôt la méthode ToString() pour avoir une représentation de l'objet'


Cet attribut est un exemple, il en existe beaucoup dans le framework .NET et vous aurez l’occasion d’en voir d’autres dans les chapitres suivants.
Remarquons que la classe s’appelle ObsoleteAttribute et peut s’utiliser soit suffixée par Attribute, soit sans, et dans ce cas, ce suffixe est ajouté automatiquement.

Grâce à l’héritage, nous allons nous aussi pouvoir définir nos propres attributs. Il suffit pour cela de dériver de la classe de base Attribute. Par exemple, dans sa forme la plus simple :

Code : C#
1
2
3
public class DescriptionClasseAttribute : Attribute
{
}


Nous pourrons alors utiliser notre attribut sur une classe :

Code : C#
1
2
3
4
[DescriptionClasse]
public class Chien
{
}


Super, même si ça ne nous sert encore à rien :p .

Problème, j’ai appelé mon attribut DescriptionClasse et je peux quand même l’utiliser sur une méthode :

Code : C#
1
2
3
4
5
6
7
8
[DescriptionClasse]
public class Chien
{
    [DescriptionClasse]
    public void Aboyer()
    {
    }
}


Heureusement, il est possible d’indiquer des restrictions sur les attributs en indiquant par exemple qu’il n’est valable que pour les classes. Cela se fait avec … un attribut :D :

Code : C#
1
2
3
4
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class DescriptionClasseAttribute : Attribute
{
}


Ici par exemple, j’indique que mon attribut n’est valable que sur les classes et qu’il est possible de le mettre plusieurs fois sur la classe. Le code précédent où l’attribut est positionné sur une méthode ne compilera donc plus, avec l’erreur de compilation suivante :

Code : Console
L'attribut 'DescriptionClasse' n'est pas valide dans ce type de déclaration. Il n'est valide que dans les déclarations 'class'.


Ce qui nous convient parfaitement. :)

Il existe d’autres attributs qui permettent par exemple de n’autoriser des attributs que sur les méthodes.


Il est intéressant de pouvoir fournir des informations complémentaires à notre attribut. Pour cela, on peut rajouter des propriétés, ou avoir une surcharge complémentaire du constructeur, comme on l’a fait pour le message de l’attribut Obsolete :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class DescriptionClasseAttribute : Attribute
{
    public string Description { get; set; }

    public DescriptionClasseAttribute()
    {
    }

    public DescriptionClasseAttribute(string description)
    {
        Description = description;
    }
}


Ce qui nous permettra d’utiliser notre attribut de ces deux façons :

Code : C#
1
2
3
4
5
[DescriptionClasse(Description = "Cette classe correspond à un chien")]
[DescriptionClasse("Elle dérive de la classe Animal")]
public class Chien : Animal
{
}


C’est très bien ces attributs, mais nous ne sommes pas encore capables de les exploiter. Voyons comment le faire.

La réflexion

La réflexion en C# ne donne pas mal à la tête, quoique …
C’est une façon de faire de l’introspection sur nos types de manière à obtenir des informations sur eux, c'est-à-dire sur les méthodes, les propriétés, etc. La réflexion permet également de charger dynamiquement des types et de faire de l’introspection sur les assemblys.
C’est un mécanisme assez complexe et plutôt couteux en termes de temps. Le point de départ est la classe Type qui permet de décrire n’importe quel type. Le type d’une classe peut être obtenu grâce au mot-clé typeof :

Code : C#
1
Type type = typeof(string);


Cet objet Type nous permet d’obtenir plein d’informations sur nos classes au moment de l’exécution. Par exemple si le type est une classe, c’est-à-dire ni un type valeur, ni une interface, alors le code suivant :

Code : C#
1
2
Type type = typeof(string);
Console.WriteLine(type.IsClass);


nous renverra true. En effet, la propriété IsClass permet d’indiquer si le type est une classe.
Nous pouvons aussi obtenir le nom de méthodes grâce à la méthode GetMethods() :

Code : C#
1
2
3
4
5
Type type = typeof(string);
foreach (MethodInfo infos in type.GetMethods())
{
    Console.WriteLine(infos.Name);
}


Qui nous donne :

Image utilisateur


Pas très exploitable comme résultat, mais bon c’est pour l’exemple. :) Il y a encore pleins d’infos dans cet objet Type, qui permettent d’obtenir ce que nous voulons.
Cela est possible grâce aux métadonnées qui sont des informations de descriptions des types.

Vous me voyez venir… eh oui, nous allons pouvoir obtenir nos attributs !
Pour connaitre la liste des attributs d’un type, on peut utiliser la méthode statique Attribute.GetCustomAttributes(), en lui passant en paramètre le System.Type qui est hérité de la classe abstraite MemberInfo. Si l’on souhaite récupérer un attribut particulier, on peut le passer en paramètre de la méthode :

Code : C#
1
Attribute[] lesAttributs = Attribute.GetCustomAttributes(type, typeof(DescriptionClasseAttribute));


Ceci va nous permettre de récupérer les attributs de notre classe et en l’occurrence, d’obtenir leurs description. C’est ce que permet le code suivant :

Code : C#
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class Program
{
    static void Main(string[] args)
    {
        new DemoAttributs().Demo();
    }

}

public class DemoAttributs
{
    public void Demo()
    {
        Animal animal = new Animal();
        Chien chien = new Chien();

        VoirDescription(animal);
        VoirDescription(chien);
    }

    public void VoirDescription<T>(T obj)
    {
        Type type = typeof(T);
        if (!type.IsClass)
            return;
        Attribute[] lesAttributs = Attribute.GetCustomAttributes(type, typeof(DescriptionClasseAttribute));
        if (lesAttributs.Length == 0)
            Console.WriteLine("Pas de description pour la classe " + type.Name + "\n");
        else
        {
            Console.WriteLine("Description pour la classe " + type.Name);
            foreach (DescriptionClasseAttribute attribut in lesAttributs)
            {
                Console.WriteLine("\t" + attribut.Description);
            }
        }
    }
}


Nous commençons par instancier un objet animal et un objet chien. Puis nous demandons leurs descriptions.
La méthode générique VoirDescription() s’occupe dans un premier temps d’obtenir le type de la classe grâce à typeof. On s’octroie une vérification permettant de nous assurer que le type est bien une classe. Ceci permet d’éviter d’aller plus loin car de toute façon, notre attribut ne s’applique qu’aux classes. Puis nous utilisons la méthode GetCustomAttributes pour obtenir les attributs de type DescriptionClasseAttribute. Et s’il y en a, alors on les affiche.

Ce qui donne :

Code : Console
Pas de description pour la classe Animal

Description pour la classe Chien
        Elle dérive de la classe Animal
        Cette classe correspond à un chien


Voilà pour les attributs, vous aurez l’occasion de rencontrer des attributs du framework .NET un peu plus loin dans ce tutoriel, mais globalement, il est assez rare d’avoir à créer soi-même ses attributs.

En ce qui concerne la réflexion, il faut bien comprendre que le sujet est beaucoup plus vaste que ça. Nous avons cependant vu ici un aperçu de ce que permet de faire la réflexion, en illustrant le mécanisme d’introspection sur les attributs.

En résumé


  • Il est possible d'empêcher une classe d'être dérivée grâce au mot-clé sealed.
  • Le garbage collector est le mécanisme permettant de libérer la mémoire allouée sur le tas managé qui n'est plus référencée dans une application.
  • Le mot-clé yield permet de créer des énumérateurs facilement et participe au mécanisme d'exécution différée.
  • Le framework .NET gère les différents formats des chaînes grâce à la culture de l'application.
  • La réflexion est un mécanisme d'introspection sur les types permettant d'obtenir des informations sur ces derniers lors de l'exécution.
Chapitre précédent Sommaire Chapitre suivant

Partager

Il n'y a pas encore de commentaire pour ce tuto.