0% ont trouvé ce document utile (0 vote)
5 vues90 pages

Introduction à C# et .NET Framework

Transféré par

jason.bourlet
Copyright
© All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats PDF, TXT ou lisez en ligne sur Scribd
0% ont trouvé ce document utile (0 vote)
5 vues90 pages

Introduction à C# et .NET Framework

Transféré par

jason.bourlet
Copyright
© All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats PDF, TXT ou lisez en ligne sur Scribd

1. C# et l'architecture .

NET

1.1. Introduction

.NET tourne dans les environnement Microsoft à partir de Windows98. Pour écrire du code
utilisant .NET, vous pouvez installer le SDK .NET téléchargeable sur le site Microsoft ou l'outil
de développement Visual studio .NET (éventuellement en version Express téléchargeable
gratuitement). Pour exécuter votre application sur une plate-forme donnée, vous devez avoir
installé sur cette plate-forme le rutine .NET connu aussi sous le nom de Common Language
Runtime (CLR).
Avant d'être exécuté par le CLR, tout code source développé en C# doit être compilé dans un
langage intermédiaire appelé MS-IL (Microsoft Intermediate Language). Ce code est à son tour
compilé en temps réel par le CLR en code spécifique à une plate-forme donnée. Actuellement,
.NET n'est disponible que pour Windows mais il existe un projet de création d'une implémentation
Open Source. Vous pourrez trouver plus de documentations à l'adresse [Link]
Sous l'impulsion de Novell cette solution a connu une seconde jeunesse et a été portée sous
androïd et IOS sous le nom de Xamarin.

1.2. Premier programme en C#

using System;
1.
2. namespace MyNameSpace
3. {
4. public class MyFirstApp
5. {
6. public static void Main()
7. {
8. // Une simple ligne de commentaire
9. /* Plusieurs lignes
10. De
11. commentaire */
12. [Link]("Bonjour à tous");
13. }
14. }
15. }

En Visual studio 6.0, les habitués de la programmation en C++ ont souvent utilisés les MFC
(Microsoft Foundation Class) offrant des possibilités rapides de développement pour les
applications orientées Windows. En C#, on retrouve également des bibliothèques de classes de
base en code IL (appelé aussi code managé). L'ensemble de ces bibliothèques se retrouvent dans
ce que l'on appelle le .NET Framework.
C# est un langage orienté objet et de ce fait les programmes doivent être placés dans des classes.
Nous retrouvons à la ligne 4 notre classe MyFirstApp. La déclaration de la classe s'effectue au
moyen du mot clef class
A la ligne 1, nous déclarons que nous utilisons un espace de nom appelé System tandis qu'à la
ligne 2, nous déclarons notre propre espace de nom MyNameSpace dans lequel nous déclarons
notre classe. Les espaces de noms sont un moyen d'éviter les collisions de noms entre classes.
Deux classes appartenant à des espaces de noms différents peuvent porter le même nom sans que
cela ne pose problème. Un espace de noms n'est rien d'autre qu'un regroupement de types de
données dont le nom sera automatiquement préfixé par celui de l'espace de noms. Il est possible
d'imbriquer les espaces de nom.
A la ligne 12, nous appelons la méthode WriteLine qui appartient à la classe de base Console
définie elle même dans l'espace de nom System (c'est dans cet espace de noms que résident les
types .NET les plus souvent utilisés).
La syntaxe complète d'accès à la méthode WriteLine pourrait être:
[Link]("bonjour à tous");
Pour éviter la complexité d'écriture du code, on peut définir les espaces de noms avec lesquels on
travaille pour ainsi limiter l'écriture à [Link]("bonjour à tous");

Lorsque nous écrivons un programme DOS en C++, nous devons définir une fonction void main()
qui sera appelée lors de l'exécution de ce programme. Si c'est un programme Windows, la
fonction doit s'appeler WinMain().
En C#, les exécutables (applications console, applications et services Windows) doivent avoir
comme point d'entrée la méthode Main(). (Notons la majuscule pour le nom de la fonction). Cette
méthode doit retourner soit un entier, soit rien se traduisant par le mot clef void.

La compilation sous Visual .NET peut s'effectuer en appuyant sur les touches Ctrl-Shift-B. Cette
compilation produira un exécutable dont l'exécution en appuyant sur les touches Ctrl-F5 produira
l'affichage Bonjour à tous.

2. Les variables simples.

2.1. Introduction.

Pour des questions de sécurité, le langage C# est un langage très typé. Les variables sont déclarées
comme étant d'un type particulier et chaque variable est contrainte à contenir uniquement une
valeur du type déclaré.
Les variables peuvent contenir soit des types valeur ou des types référence ou éventuellement des
pointeurs.
Nous retrouvons au niveau du langage intermédiaire IL des types prédéfinis permettant ainsi de
résoudre au niveau du .NET les problèmes liés à l'existence des différents langages de base C# ou
[Link]. Ces types prédéfinis que l'on appelle CTS (Common Type System) se retrouvent au
niveau du .NET Framework sous forme de structure.
Prenons la déclaration d'un entier codé sur 32 bits:

En C#, nous retrouvons la syntaxe int a


En [Link], nous retrouvons la syntaxe Integer a
Ces deux syntaxes d'un point de vue CTS se ramènent à la même déclaration équivalente qui
serait: System.Int32 a. La variable a est en fait ici considérée comme une instance de la structure
Int32 appartenant à l'espace de nom System. De ce fait, on peut facilement imaginer que cette
structure offre des méthodes qui peuvent être appelées par l'instance. Nous pouvons en effet
retrouver [Link]() permettant d'assurer la conversion de l'entier vers une chaîne de caractères.

2.2. Les types valeur.

Il est possible en C# de définir nos propres types valeur en les déclarant comme énumérations
ou comme structures. La table suivante donne des informations sur les types valeur prédéfinis.

2.2.1. Les types entiers.


Nom CTS Description Valeurs possibles
sbyte [Link] Entier signé sur 8 bits -128:127
short System.Int16 Entier signé sur 16 bits -32768:32767
int System.Int32 Entier signé sur 32 bits -231:231-1
long System.Int64 Entier signé codé sur 64bits -263:263-1
byte [Link] Entier non signé codé sur 8 bits 0:255
ushort System.UInt16 Entier non signé sur 16 bits 0:65535
uint System.UInt32 Entier non signé sur 32 bits 0:232-1
ulong System.UInt64 Entier non signé codé sur 64 bits 0:264-1

2.2.2. Les types flottants.

Nom CTS Description Valeurs possibles


float [Link] Nombre en virgule flottante sur
32 bits en simple précision
double [Link] Nombre en virgule flottante sur
64 bits en double précision

2.2.3. Le type décimal.

Nom CTS Description Valeurs possibles


decimal [Link] Nombre en virgule flottante sur
128 bits en haute précision

2.2.4. Le type booléen.

Nom CTS Valeurs possibles


bool [Link] true ou false

Contrairement au langage C ou un booléen n'existe pas comme type mais provient de la


création d'un type énuméré, on retrouve en C# un type booléen natif. En C#, il est impossible
de convertir implicitement les valeurs bool en entiers.

2.2.5. Le type caractère (char).

Nom CTS Description


char [Link] représente un seul caractère codé
sur 16 bits(Unicode)

Du fait qu'un caractère soit codé sur 16 bits, il n'est pas possible d'effectuer une conversion
implicite vers un type byte.

2.3. La déclaration des variables de type valeur et leur initialisation.

Nous pouvons déclarer une variable de type valeur à l'aide de la syntaxe suivante:

datatype identifier;

Exemples:
int a;
bool y;

On peut initialiser une variable en utilisant l'opérateur d'affectation '='. Cette initialisation peut
s'effectuer en même temps que la déclaration en utilisant la forme suivante:

int a = 10;
bool y = true;

Par défaut, une constante entière est considérée comme le compilateur comme étant de type entier.
Pour forcer le compilateur à considérer les constantes d'un autre type, il faudra lors de
l'initialisation utiliser les syntaxes suivantes:

uint x = 123U; //U comme unsigned


long y = 1235L; //L comme long
ulong z = 256UL //UL unsigned long

Par défaut, une constante réelle est considérée en double précision. Pour forcer le compilateur à
considérer les constantes d'un autre type, il faudra lors de l'initialisation utiliser les syntaxes
suivantes:

decimal a = 13.20M;
float b = 15.75F; //F comme float

Par défaut, certaines variables sont initialisées et d'autres ne le sont pas. Le compilateur C# met
l'accent sur la sécurité et exige que toutes les variables soient initialisées avec une valeur de départ
avant que le programmeur puisse s'y référer dans une opération. Les violations à ces règles sont
traitées comme des erreurs lors de la compilation.

Voici les règles d'initialisation par défaut:

 Les variables correspondant à des membres dans une classe ou une structure sont remises à
zéro par défaut au moment de leur création si elles ne sont pas explicitement initialisées.
 Les variables qui sont locales dans une méthode doivent être explicitement initialisées.

Reprenons l'exemple suivant:

using System;
1.
2. namespace MyNameSpace
3. {
4. public class MyFirstApp
5. {
6. public static void Main()
7. {
8. int x;
9. [Link](x);
10. }
11. }
12. }

La ligne 9 présente une erreur à la compilation car la variable x est locale à la méthode Main( ) et
n'est pas initialisée par défaut. Pour éviter cette erreur, il suffit de remplacer la ligne 8 par
int x = 0;
2.4. La portée des variables.

Ce que l'on entend par portée, c'est la partie de code dans laquelle une variable est accessible. On
peut déterminée la portée selon les règles suivantes:

 Un membre d'une classe est visible aussi longtemps que la classe qui le contient est dans la
portée.
 Une variable locale est visible uniquement dans la partie de code ou elle a été déclarée, ce qui
correspond à la fermeture de l'accolade du bloc ou elle a été déclarée. Peuvent être considérée
comme variables locales celles qui sont déclarées dans une instruction for et while.

2.5. Les types référence.

2.5.1. Présentation.

Les types référence prédéfinis sont objet et string où objet est la classe de base de tous les
autres types. Des nouveaux types référence peuvent être définis en utilisant les mots clefs
class, interface ou delegate qui seront étudiés dans un paragraphe ultérieur.
Le type référence stocke l'adresse de la zone mémoire occupée par l'objet qu'il référence.
L'objet occupe une zone que l'on appelle le tas managé tandis que l'adresse est placée dans la
pile au même titre qu'un type valeur.

Exemple:

1. using System;
2.
3. namespace MyNameSpace
4. {
5. class MyFirstObjet
6. {
7. public int a;
8. public void print()
9. {
10. [Link]("Contenu du membre a:"+[Link]());
11. }
12. }
13. class MyFirstApp
14. {
15. static void Main()
16. {
17. MyFirstObjet x = new MyFirstObjet();
18. x.a = 10;
19. [Link]();
20. }
21. }
22. }

Nous retrouvons dans notre exemple une classe que l'on va instancier grâce à l'opérateur new.
Cet opérateur permet la création de l'instance et retourne une adresse qui sera placée dans la
variable x de type référence. On retrouve l'opérateur new mais nul part dans notre code
l'opérateur delete qui permettrait de libérer l'espace alloué à l'objet.
Ce n'est pas une erreur de programmation car, dès qu'un objet présent sur le tas managé n'est
plus instancié, c'est le ramasse-miettes (garbage collector) qui s'occupe de libérer
automatiquement cet espace.
Nous pouvons étendre notre exemple aux aspects suivants:

13. class MyFirstApp


14. {
15. static void Main()
16. {
17. MyFirstObjet x = new MyFirstObjet();
18. MyFirstObjet y = x;
19. x.a = 10;
20. [Link]();
21. }
22. }

L'exécution du code modifié dans notre code modifié de la sorte provoque le même affichage
prouvant que x et y pointent vers le même objet.

2.5.2. Le type prédéfini string.

Les programmeurs en C ont certainement eu recours aux fonctions de gestion de chaînes de


caractères dont les prototypes se trouvaient définis dans le fichier d’en-tête string.h.
En C++, la manipulation des chaînes se simplifie en ayant recours aux instanciations de la
classe Cstring. Exemples :

En langage C : En Visual C++

char first[50] ; Cstring first, second;


char second[50] ;
strcpy(first, "bonjour"); first = "bonjour";
strcpy(second," à tous"); second= " à tous";
strcat(first,second); first = first + second;

En C#, voici un exemple complet:

using System;

namespace MyFirstApp
{
class MyFirstClass
{
static void Main()
{
string first = "bonjour";
string second = " à tous";
string result = first + second;
[Link]("resultat:" + result);
}
}
}

L'exécution du programme donne alors l'affichage: resultat:bonjour à tous. Comme string est
un type prédéfini, il n'est pas nécessaire dans ce cas d'utiliser l'opérateur new.
Sachant que string est un type référence, qu'en est t il de l'exemple suivant:
1. using System;
2.
3. namespace MyFirstApp
4. {
5. class MyFirstClass
6. {
7. static void Main()
8. {
9. string first = "chaîne initiale";
10. string second = first;
11. second = "chaîne modifiée";
12. [Link]("resultat:" + first);
13. }
14. }
15. }

La ligne 9 nous permet de créer un type référencé qui contiendra l'adresse de l'instance de
l'objet String placé sur le tas mangé. A la ligne suivante, nous créons un deuxième type
référence que nous initialisons avec la même adresse. On peut donc supposer que les deux
types pointent vers le même objet et que la modification de l'un induira automatiquement une
modification sur l'autre.
Bien que le type string soit un type référence, sa fonctionnalité est toute différente. Lorsque
l'objet est initialisé, il a une taille permettant de contenir juste la chaîne de départ, à savoir
chaîne initiale. Si on veut remplacer cette chaîne par une autre, une nouvelle instance est créée
et la nouvelle adresse se retrouve alors dans la variable second.
L'affichage résultant de l'exécution du programme donnera donc:
resultat:chaîne initiale.

3. Les tableaux.

3.1. Introduction.

L'approche que l'on fait des tableaux en C et en C++ est tout à fait différente de celle du C# : en
C#, un tableau est une instance de la classe de base .NET [Link]. Cette instanciation
permet de mieux contrôler les accès et d'éviter les erreurs fréquentes de débordement des limites
du tableau. Ces erreurs apparaissaient lors de l'exécution du programme et provoquaient souvent
un plantage de ce dernier.

3.2. Les tableaux à 1 dimension.

Pour déclarer un tableau en C#, il suffit de fixer un jeu de crochets droits à la fin du type de
variable des éléments individuels. Notons que tous les éléments d'un tableau sont du même type de
données.

Exemple:

1. using System;
2.
3. namespace MyFirstApp
4. {
5. class MyFirstClass
6. {
7. static void Main()
8. {
9. int[]tabint = new int[10];
10. float[]tabfloat = null;
11. tabint[0] = 10;
12. tabfloat = new float[10];
13. tabfloat[9] = 4.52F;
14. [Link]("entier[0]:"+tabint[0].ToString());
15. [Link]("flottant[9]:"+tabfloat[9].ToString());
16. }
17. }
18. }

Nous retrouvons en ligne 9 la déclaration d'un tableau d'entier. La variable tabint est de type
référence : nous utilisons en effet l'opérateur new pour instancier la classe. Dans notre exemple,
nous avons déclaré un tableau d'entier de 10 cases. Tout comme pour le C et le C++, l'indice le
plus petit avec lequel on peut accéder au tableau est 0. Nous pourrons donc aller de l'indice 0 à
l'indice 9.
A la ligne 10, nous avons déclaré uniquement le type référence et nous l'avons initialisé avec
l'adresse null tandis que l'instanciation s'effectue à la ligne 12.
Si vous essayez d'accéder à un indice dépassant les possibilités de taille du tableau, une exception
sera générée à l'exécution du programme.

Vous pouvez également déclarer un tableau de chaînes de caractères en utilisant la syntaxe


suivante:

1. using System;
2.
3. namespace MyFirstApp
4. {
5. class MyFirstClass
6. {
7. static void Main()
8. {
9. string[] tabchaine = new string[10];
10. string[] tabsec = {"un", "deux", "trois"};
11. [Link](tabsec[2]);
12. }
13. }
14. }

La ligne 9 permet de déclarer un tableau de 10 chaînes de caractères. La ligne 10 permet de


déclarer et d'initialiser en une seule ligne d'instruction le tableau. L'exécution du programme
provoquera l'affichage suivant: trois.
Il n'est pas possible de modifier la taille d'un tableau une fois qu'il a été instancié.

Etant donné qu'un tableau correspond à un objet que l'on doit instancier, on pourra donc avoir
accès à une série de méthodes statiques permettant de gérer ce tableau de chaînes de caractères. Si
vous désirez trier le tableau, vous pourrez utiliser la méthode Sort( ). Attention car une méthode
statique ne peut être appelée que par le nom de la classe.

Exemple de modification du code ci dessus:

[Link](tabsec);
[Link](tabsec[2]);

Le résultat de l'exécution du programme donnera donc: un


Voici un autre programme permettant le passage en revue du tableau et l'affichage des différentes
chaines.

1. using System;
2.
3. namespace MyFirstApp
4. {
5. class MyFirstClass
6. {
7. static void Main()
8. {
9. string[] tabchaine = new string[10];
10. string[] tabsec = {"un", "deux", "trois"};
11. [Link](tabsec);
12. foreach(string compte in tabsec)
13. [Link](compte);
14. }
15. }
16. }

L'exécution du programme donnera l'affichage suivant:

deux
trois
un

Les autres tableaux n’échappent pas à cette façon de faire.

1. class MyFirstClass
2. {
3. static void Main()
4. {
5. int[] i = new int[] {1,2};
6. int[] j = {1,2};
7. [Link](i[0]);
8. [Link](j[0]);
9.
10. }
11. }

3.3. Les tableaux multidimensionnels.

C# supporte deux types de tableaux multidimensionnels: un tableau rectangulaire appelé aussi


matrice et un tableau orthogonal (appelé jagged). Un tableau rectangulaire a un nombre constant
de colonne pour l’ensemble des lignes tandis qu’un tableau orthogonal peut avoir un nombre
variable de colonne pour chacune des lignes qui le compose.

a) les tableaux rectangulaires.

La syntaxe suivante permet de déclarer un tableau rectangulaire de 2 lignes et 4 colonnes :


int[,] tabrect = new int[2,3]; // la virgule indique que le tableau a deux dimensions.

Tout comme pour les tableaux unidimensionnels, la déclaration peut se faire lors de l’initialisation
en utilisant la syntaxe suivante :
int[,] tabrect = {{1, 2, 3}, {4, 5, 6}};
string[,] tabrect = {{"Dupond","Albert"},{"Dubart","Eric"},{"Durant","Thierry"}};

Le tableau précédent possède 2 lignes et 3 colonnes.


Etant donné que le tableau multidimensionnel correspond à une instance de la classe
[Link], nous disposons de méthodes permettant de récupérer des informations sur le
tableau ou de pouvoir demander d’en trier le contenu.

Exemple :

1. class MyFirstClass
2. {
3. static void Main()
4. {
5. int[,] tabint = new int[2,3];
6. int [,] tabint2 = {{4,7,5},{8,2,12}};
7. for (int i=0; i<=[Link](0);i++)
8. for (int j=0;
j<=[Link](1);j++)
9. [Link](tabint2[i,j]);
10. }
11. }

La ligne 7 et 8 reprennent l’appel à la méthode GetUpperBound : la méthode renvoie l’indice


maximal de la dimension envoyée en paramètre. Ceci nous évite de devoir mémoriser les indices
maximaux autorisés pour l’ensemble des tableaux avec lesquels on nous travaillons.

b) Les tableaux orthogonaux.

La syntaxe suivante nous permet de déclarer un tableau orthogonal :


int[][] tabjag = new int[2][];
tabjag[0] = new int [4];
tabjag[1] = new int [6];
Attention à la différence de syntaxe:
int[,] x = new int [4,7] ; permet la création d’un tableau rectangulaire
int[][] y = new int[2][] ; permet la création d’un tableau orthogonal comprenant deux lignes mais
le nombre de colonne est variable. Ce type de tableau peut être vu comme un tableau de tableau.

Une autre syntaxe permettant l’initialisation en même temps que la déclaration nous donnerait
donc :

int[][] tabjag = new int[][] {new int[] {1, 2, 3, 4}, new int[] {5, 6, 7, 8, 9, 10}};
Tout comme les tableaux rectangulaires, l'on pourra utiliser les différentes méthodes mises à notre
disposition.

1. class MyFirstClass
2. {
3. static void Main()
4. {
5. int[][] tabjag = new int[][] { new int[]{1, 2,
3, 4},new int[] {5, 6, 7, 8, 9, 10}};
6. for (int i=0; i<[Link](0); i++)
7. for (int j=0;
j<tabjag[i].GetLength(0);j++)
8. [Link](tabjag[i][j]);
9.
10. }
11. }

A la ligne 6 et 7, nous retrouvons l'appel à la méthode GetLength qui renvoie le nombre


d'éléments que l'on retrouve dans une dimension passée en paramètre.

3.4. Les conversions de type.

Bien que fort semblable à ce que l'on connaît en langage C, il nous a semblé important de redonner
des notions simples sur les conversions de type. Vous devez retenir que lorsque l'on essaie de
copier le contenu d'une variable dans une autre par un opérateur d'affectation, si vous ne risquez
pas de perdre d'informations utiles, la conversion est dite implicite et le compilateur ne génère pas
d'erreurs.
Si l'opération d'affectation risque de vous faire perdre des informations utiles, le compilateur
nécessite l'utilisation d'un opérateur de transtypage.

Exemple:

long a = 10;
int b = a;

Bien que la valeur 10 puisse tenir dans la variable b, le compilateur n'accepte pas que vous placiez
dans un entier codé sur 32 bits, le contenu d'un entier codé sur 64 bits. Pour que cela puisse être
compilé sans erreur, vous devez utiliser l'opérateur de transtypage sous la syntaxe suivante:

long a = 10;
int b = (int) a;

Si vous souhaitez qu'un transtypage forcé ne provoque pas de débordement qui pourrait rendre
l'exécution de votre programme imprévisible, il est possible de forcer le runtime à lever une
exception de dépassement en cas de besoin.

Exemple:

int a = 525;
byte b = (byte) a;
[Link]("contenu de b: "+[Link]( ));

Le compilateur ne génère pas d'erreur et pourtant un tel code exécuté donne un résultat prévisible
mais qui peut ne pas vous satisfaire. Vous voyez apparaître sur votre écran
contenu de b: 13. Vous obtenez en fait 525 modulo 256 car un byte est codé sur 8 bits.

Si vous souhaitez que le runtime génère une exception, il faudra utiliser la syntaxe suivante:

int a = 525;
byte b = checked ((byte) a);
[Link]("contenu de b: "+[Link]( ));

Voici un tableau des conversions qui sont implicites:

De Vers
sbyte short, int, long, float, double, decimal
byte short, ushort, int, uint, long, ulong, float, double,
decimal
short int, long, float, double, decimal
ushort int, uint, long, ulong, float, double, decimal
int long, float, double, decimal
uint long, ulong, float, double, decimal
long, ulong float, double, decimal
float double
char ushort, int, uint, long, ulong, float, double,
decimal

Pour les conversions entre un nombre et une chaîne, nous disposons des méthodes que nous avons
eu l'occasion d'utiliser à maintes reprises. La classe de base Objet implémente en effet une
méthode appelée ToString( ).
Pour parser une chaîne afin de retrouver une valeur numérique ou booléenne, nous pouvons
utiliser la méthode Parse( ) prise en compte par tous les types valeur prédéfinis:

string s = "100,20";
float x = [Link](s);
[Link](x);

4. Les énumérations
Une énumération est un type entier défini par l'utilisateur. La syntaxe est identique à celle que l'on
retrouve dans le langage C ou C++. Lorsque nous définissons une énumération, nous fournissons
du texte qui est alors utilisé comme constante pour leurs valeurs correspondantes. Voici un
exemple:

1. class MyFirstClass
2. {
3. public enum DayOfWeek
4. {
5. Dimanche = 1,
6. Lundi,
7. Mardi,
8. Mercredi,
9. Jeudi,
10. Vendredi,
11. Samedi
12. }
13. static void Main()
14. {
15. DayOfWeek Day;
16. Day = [Link];
17. Array dayArray = [Link](typeof(DayOfWeek));
18. foreach (DayOfWeek Days in dayArray)
19. {
20. [Link]("Le jour n°{0} est le
{1}",(int)Days,Days);
21. }
22. }
23. }

Un type énuméré dépend de [Link] et de ce fait, toute variable de type énuméré doit être
considérée comme un objet. Cette classe possède des méthodes statiques que l'on utilise dans notre
code pour notamment récupérer un tableau des valeurs et des constantes d'un type énuméré
particulier. Il s'agit de [Link] que l'on retrouve à la ligne 17.
typeof est un opérateur qui retourne un objet Type représentant un type spécifié. Dans notre cas,
typeof( DayOfWeek) renvoie un objet Type représentant le type [Link].
Nous retrouvons à la ligne 18 l'instruction foreach qui permet de passer en revue un tableau
d'objet, et de les retourner un par un au moyen de l'objet Days.
Nous retrouvons à la ligne 20 une syntaxe différente de la méthode WriteLine qui va nous
permettre d'envisager l'utilisation de plusieurs arguments.
[Link]("Le jour n°{0} est le {1}",(int)Days,Days);
{0} correspond au premier argument dans la liste c-à-d (int)days.
{1} correspond au deuxième argument dans la liste c-à-d Days.

5. C# orienté objet.

5.1. Syntaxe des classes.

La syntaxe des classes en C# est fort similaire à celle du C++. Nous y retrouvons des déclarations
de variables membres et des déclarations de méthodes encore appelées fonctions membre. Il est
important malgré tout de signaler que l’utilisation du mot clef struct lors de la déclaration de la
classe a d’autres effets que ceux présentés en C++, à savoir de rendre par défaut les membres et les
méthodes publics. Nous y reviendrons lors d’un paragraphe ultérieur et nous n’utiliserons dans un
premier temps que le mot clef class.
Les modificateurs globaux de type public, private ou protected n’existent plus dans le sens où
nous devons maintenant faire précéder chaque membre ou méthode de ce mot clef pour l’en
affecter.

Class MyFirstClass
{
private int UnMembre ;
public void UneMethode(bool UnParamètre)
{
}
}

En C++ les méthodes pouvaient être définies à l’extérieur de la classe en utilisant l’opérateur de
résolution de portée ; en c# la définition des méthodes doit se réaliser à l’intérieur de la classe.
La création d’un objet en C# s’effectue toujours par référence c-à-d que vous devez
nécessairement utiliser l’opérateur new sous la forme suivante :

MyFirstClass MonObject = new MyFirstClass( );

Pour rappel, d’un point de vue occupation mémoire, la référence est placée sur la pile tandis que
l’objet référencé est placé sur le tas managé.

5.2. L’héritage simple.

La notion d’héritage a été fortement simplifiée par rapport à ce que l’on connaît du C++ : il n’y a
plus que de l’héritage simple, l’héritage multiple ayant été abandonné. Voici la syntaxe à utiliser
pour mettre en place l’héritage simple :

Class MyDerivedclass : MyBaseClass


{
//membres et fonctions à placer ici.
}

Nous remarquerons à ce niveau ci une différence avec le C++ car l’héritage ne comprend plus de
modificateur d’accessibilité.

5.3. La déclaration des méthodes.

En C#, la déclaration des méthodes comprend le modificateur d’accès. Nous retrouvons la syntaxe
suivante lors de la déclaration :

[modificateur] type_de_retour Identificateur([paramètres])


{
// corps de la fonction.
}

Nous retrouverons les modificateurs suivants :

Static. Il existe deux grandes catégories de méthodes : les méthodes dites instanciées et les
méthodes dites de classe. Les méthodes instanciées n’existent que si l’on a créé une instance de la
classe et elles sont appelées par l’objet lui même sous la forme : objet.méthode([paramètres]) ;.
Les méthodes de classe déclarées avec le mot clef static peuvent être appelées même s’il n’existe
aucune instance de la classe sous la syntaxe suivante : Nom_de_classe.Méthode ( [paramètres]) ;.

Prenons comme exemple une classe permettant la gestion des nombres complexes.
Sans mettre en œuvre la surcharge des opérateurs, nous allons intégrer dans notre classe une
méthode permettant d'effectuer une opération d'addition sur deux complexes. En C++, nous
pourrions utiliser une fonction amie de sorte que les objets passés en paramètre puisse accéder aux
membres protégés de la classe mais en C#, cette solution n'est pas autorisée.
Nous devons donc nous orienter vers des méthodes statiques. Un peu dans le même esprit que
pour les fonctions amies, l'ensemble des objets doit être passé en paramètre du fait que la méthode
statique est appelée par le nom de la classe et pas par une instance.

1 namespace ConsoleApp
2 {
3 class Complexe
4 {
5 public int reel;
6 public int image;
7 public Complexe(int reel, int image)
8 {
9 [Link]=reel;
10 [Link]=image;
11 }
12 public static Complexe MultComplexe(Complexe x, Complexe y)
13 {
14 int reel=[Link]*[Link]*[Link];
15 int image=[Link]*[Link]+[Link]*[Link];
16 Complexe tmp = new Complexe (reel,image);
17 return tmp;
18 }
19 }
20 class Class1
21 {
22 static void Main(string[] args)
23 {
24 Complexe ca = new Complexe(10,20);
25 Complexe cb = new Complexe(20,30);
26 Complexe cc = [Link](ca,cb);
27 [Link]("{0}+i{1}",[Link](),
[Link]());
28 }
29 }

Remarque importante de syntaxe:

En C++, l'opérateur this renvoyait dans une méthode un pointeur sur l'instance l'ayant appelée. En
C#, nous ne parlons plus de pointeurs mais de référence et de ce fait, this n'échappe pas à la règle.
D'un point de vue syntaxe, on retrouve la différence suivante aux lignes 9 et 10 :

En C++ En C#
this->reel [Link]
this->image [Link]

new, virtual, override. Ces modificateurs concernent l’héritage. Lorsque le mot clef virtual est
utilisé pour déclarer une méthode d’une classe de base, cela signifie qu’elle est surchargée dans les
classes dérivées. Dans les classes dérivées, la surcharge de la méthode de la classe de base doit
être déclarée en utilisant le mot clef override. Notons que pour qu’une méthode puisse surcharger
une autre méthode, la méthode surchargée ne doit pas être de type static et qu’elle doit être
déclarée en utilisant le mot clef virtual, abstract ou override.
L’opérateur new permet à une méthode d’une classe dérivée de cacher une méthode d’une classe
de base sans pour autant nécessiter une déclaration particulière pour la méthode cachée.

Supposons une classe de base appelée rectangle et une classe dérivée appelée carré. Supposons
dans la classe de base une méthode appelée surface qui doit être surchargée dans la classe dérivée.

• Utilisation des mots clefs virtual et override:

1 namespace ConsoleApp
2 {
3
4 class Rectangle
5 {
6 protected float longueur;
7 protected float largeur;
8 public Rectangle(float largeur, float longueur)
9 {
10 [Link] = largeur;
11 [Link] = longueur;
12 }
13 public float surface()
14 {
15 return largeur*longueur;
16 }
17 }
18 class Carre:Rectangle
19 {
20 public Carre(float largeur):base(largeur,0)
21 {
22
23 }
24 public float surface()
25 {
26 return largeur*largeur;
27 }
28 }
29
30 class Class1
31 {
32 static void Main(string[] args)
33 {
34 Carre ca = new Carre(10.0F);
35 Rectangle ra = ca;
36 [Link]("surface:"+[Link]().ToString());
37 [Link]("surface:"+[Link]().ToString());
38 }
39 }
40 }

Commentaires:

Pour rappel, en C++, un pointeur d'une classe de base peut contenir l'adresse d'une classe dérivée.
En C#, une référence d'une classe de base peut pointer vers un objet d'une classe dérivée. C'est ce
que l'on retrouve à la ligne 35. La question à se poser est de savoir quelle est la méthode qui sera
appelée à la ligne 36: est ce la méthode liée au type de la référence ou au type de l'objet sur lequel
la référence pointe.
L'exécution du code nous donne l'affichage suivant:

surface:0
surface:100
Press any key to continue

C'est bien le type de la référence et non pas le type de l'objet référencé. Si nous voulons que le
choix de la méthode se fasse sur base du type de l'objet pointé, nous devons utiliser le mot clef
virtual pour définir la méthode de la classe de base et obtenir la syntaxe suivante:

virtual public float surface()

L'analyse de l'affichage résultant de la compilation nous donne l'avertissement suivant:


warning CS0108: The keyword new is required on '[Link]()' because it
hides inherited member '[Link]()'

Actuellement, ce message n'a pas beaucoup de signification car nous n'avons pas encore dévoilé
les particularités de l'opérateur new dans la déclaration d'une méthode.
Si nous nous limitons à l'usage de l'opérateur virtual, nous obtenons à la compilation le message
suivant:
warning CS0114: '[Link]()' hides inherited member
'[Link]()'. To make the current member override that implementation,
add the override keyword. Otherwise add the new keyword.

L'exécution du code donne le même résultat. Il faut donc ajouter le mot clef override dans la
déclaration de la surcharge sous la forme:

override public float surface()

L'exécution de notre programme nous permet maintenant d'obtenir l'affichage suivant:

surface:100
surface:100
Press any key to continue
• Utilisation du mot clef new

Dans le premier exemple de notre classe Complexe, la méthode surchargée cache par défaut la
méthode de la classe de base. L'utilisation du mot clef new a le même effet excepté que le
compilateur ne génère plus de message d'attention.

Abstact Une méthode abstract n’a pas d’implémentation et doit être surchargée dans toute classe
dérivée non abstraite. Evidemment, une méthode abstraite est automatiquement virtuelle. Une
interface est une classe qui ne contient que des méthodes abstraites. Cependant, de telles classes
sont déclarées avec le mot clef interface plutôt que le mot clef class. Une classe peut hériter d’une
seule classe de base mais peut hériter de plusieurs interfaces. La notion d’interface sera abordée
dans un paragraphe ultérieur.

Extern Les méthodes qui sont déclarées comme étant externes sont définies à l’extérieur en
utilisant un langage autre que C#.

5.4. Les passages de paramètres.

Dans des langages tels que C ou C++, le passage de paramètres à une fonction ou à une méthode
peut se faire par valeur, par pointeur et uniquement en C++ par référence avec l'utilisation de
l'opérateur &. En C#, l'utilisation des pointeurs n'étant pas possible, on retrouvera des passages par
valeur ou par référence.

• Passage par référence.

L'exemple suivant reprend un passage par référence bien que l'entier soit de type valeur.

1 class Class1
2 {
3 /// <summary>
4 /// The main entry point for the application.
5 /// </summary>
6 [STAThread]
7 static void Main(string[] args)
8 {
9 int b=10;
10 [Link]("Avant appel:"+[Link]());
11 change(ref b);
12 [Link]("Apres appel:"+[Link]());
13 }
14
15 static void change (ref int a)
16 {
17 a=20;
18 }
19 }

Remarque concernant la syntaxe:

La ligne 15 reprend le mot clef ref dans la déclaration du paramètre et la ligne 11 doit comprendre
obligatoirement le mot clef ref lors de l'appel.

L'exécution du code provoque l'affichage suivant:


Avant appel:10
Apres appel:20
Press any key to continue

Nous pouvons en déduire que la variable locale a est en fait une référence pointant sur le même
objet que la variable b. Toute modification du contenu de a provoque une modification du contenu
de b.

• Passage par valeur.

Idem que dans le langage C et C++.

• Le modificateur de paramètre "out".

Lorsque nous désirons qu'une fonction renvoie plusieurs valeurs au programme appelant, nous
utilisons les passages par pointeurs si nous travaillons en C++; en C#, nous utiliserons les
références. Un des points importants du C# est qu'il faut initialiser les variables même si celles-ci
n'ont pour but que de récupérer des valeurs en provenance d'une fonction appelée.
Reprenons l'exemple de la classe rectangle et modifions notre code de sorte que la fonction
surface renvoie la surface du rectangle par paramètre.

1 class rectangle
2 {
3 float largeur;
4 float hauteur;
5 public rectangle(float largeur, float hauteur)
6 {
7 [Link]=largeur;
8 [Link]=hauteur;
9 }
10 public void surface(ref float surf)
11 {
12 surf = largeur*hauteur;
13 }
14 }

15 class Class1
16 {
17 static void Main(string[] args)
18 {
19 float result;
20 rectangle ra = new rectangle(10.0F, 2.5F);
21 [Link](ref result);
22 [Link]("surface:"+[Link]());
23 }
24
25 }

Dans notre exemple, l'initialisation de la variable locale result ne devrait pas être nécessaire
puisqu'elle ne sert qu'à recevoir le résultat de l'exécution de la méthode surface.
Malgré cette certitude, le compilateur respecte la règle de réclamer l'initialisation de toute
variable locale avant son utilisation et nous obtiendrons l'erreur suivante à la compilation:
[Link](34): Use of unassigned local variable 'result'
Pour éviter ce problème, il suffit d'utiliser le mot clef out. Ce mot clef doit être utilisé lors de la
déclaration de la méthode et lors de l'appel en remplacement du mot clef ref. Il a le même effet de
produire un passage par référence mais en tolérant la variable non initialisée lors de l'appel.

surface:25
Press any key to continue

• Le modificateur de paramètre "params".

Il arrive que nous désirions faire passer un nombre variable de paramètres lors d'un appel à une
fonction; ce passage peut s'effectuer en utilisant le modificateur params. Tous les paramètres
passés lors de l'appel sont passés par valeur.

1 class Class1
2 {
3 static void Main(string[] args)
4 {
5 int a=10,b=20,c=30;
6 float result;
7 result=find(a,b,c);
8 [Link]("Le plus
grand:"+[Link]());
9 }
10 static int find(params int[]args)
11 {
12 int greater=args[0];
13 for (int i=1;i<[Link];i++)
14 {
15 if (args[i]>greater) greater=args[i];
16 }
17 return greater;
18 }
19
20 }

Remarque concernant la syntaxe:

La ligne 13 comprend un appel à la propriété Length qui renvoie le nombre de paramètres qui ont
été passés lors de l'appel de la méthode.

5.5. La surcharge des méthodes.

Tout comme pour le C++, le C# supporte la surcharge des méthodes c-à-d que plusieurs versions
d'une même méthode qui ont différentes signatures (nom, nombre des paramètres, types des
paramètres) peuvent exister dans la même classe; cette surcharge est également valable pour des
méthodes tels que les constructeurs. Le C# n'acceptant pas les paramètres par défaut, la surcharge
permet de contourner cette limitation

5.6. Les propriétés.

En C#, les propriétés représentent un concept repris du Visual Basic. Une propriété consiste en
une méthode ou paire de méthodes permettant la modification ou la lecture du contenu d'un
membre sans y accéder directement.
Pour définir une propriété en C#, nous utiliserons la syntaxe suivante:
1 class carre
2 {
3 private float cote;
4 public float Cote
5 {
6 Get
7 {
8 return cote;
9 }
10 Set
11 {
12 cote=value;
13 }
14 }
15 public carre(float cote)
16 {
17 [Link]=cote;
18 }
19 }

20 class Class1
21 {
22 static void Main(string[] args)
23 {
24 carre ca = new carre(12.5F);
25 [Link]("Lecture:"+[Link]);
26 [Link]=10.0F;
27 [Link]("valeur du cote:"+[Link]);
28 }
29
30
31 }

Si vous désirez qu'une propriété soit accessible uniquement en lecture, il suffit de conserver le
membre correspondant en privé et de ne prévoir dans votre classe que l'accesseur get.
Soit l'exercice suivant: nous imaginons une classe dans laquelle nous désirons à tout instant
pouvoir connaître le nombre d'instances qui ont été créées. Pour ce, reprenons notre classe au
point 5.2 permettant de gérer les complexes et implémentons ce membre.

1 class Complexe
2 {
3 private static int compteur=0;
4 public int reel;
5 public int image;
6 public static int Compteur
7 {
8 Get
9 {
10 return compteur;
11 }
12 }
13 public Complexe(int reel, int image)
14 {
15 compteur++;
16 [Link]=reel;
17 [Link]=image;
18 }
19 ~Complexe()
20 {
21 compteur--;
22 }
23 }
24 class Class1
25 {
26 static void Main(string[] args)
27 {
28 Complexe ca = new Complexe(10,20);
29 Complexe cb = new Complexe(20,30);
30 [Link]("Instances:"+[Link]);
31 }
32 }

Remarques:

A la ligne 3, nous retrouvons la création d'une variable membre de type statique qui est en mode
d'accès privé. Ce membre est en fait notre compteur d'instance qui doit donc être commun à
toutes les instances; c'est pourquoi nous choisissons de déclarer ce membre comme statique.
L'accès en écriture à ce membre s'effectue uniquement en interne par le constructeur qui
l'incrémente et par le destructeur qui le décrémente. Il n'est donc pas utile de rendre accessible
cette variable membre en écriture de l'extérieur de la classe. Nous plaçons dans notre classe un
accesseur que nous appelons Compteur; on ne peut pas lui donner le même nom que celui du
membre mais il est intéressant qu'il en soit le plus proche pour une question de facilité dans la
lisibilité du code. Le membre compteur présente la première lettre en minuscule et l'accesseur
Compteur la première lettre en majuscule.
Il est à remarquer l'emploi d'un accesseur de type statique qui ne dépend donc pas d'une instance
mais sera donc appelé par le nom de la classe (voir ligne 30).

5.7. La surcharge des opérateurs.

Un des intérêts du C++ est de pouvoir surcharger les opérateurs pour une question de facilité dans
la gestion des opérations arithmétiques et de comparaisons sur les objets d'une classe.
Dans notre exemple de la classe Complexe, nous avons mis en place au point 5.3 une méthode
statique permettant de pouvoir effectuer la multiplication de deux nombres complexes. Il est clair
que si nous avions pu remplacer la ligne de code [Link](ca,cb) par
cc=ca*cb; nous utilisions un opérateur déjà connu qui est l'opérateur de multiplication * et le
code ne demande que peu de commentaires excepté si nous nous efforçons à choisir des noms de
méthodes évocateurs.
La mise en place des surcharges est fort semblable à celle utilisée en C++, excepté que le C#
n'accepte pas l'utilisation de fonction amie. Les surcharges doivent donc apparaître comme des
méthodes de la classe et les différentes opérandes doivent être passées en paramètre. Le nom de la
méthode doit comprendre le mot clef operator suivi du symbole utilisé pour l'opérateur. Cette
méthode particulière doit être déclarée en static.
Considérons les deux exercices suivants:
• Remplacer la méthode statique MultComplexe par une surcharge de l'opérateur *
• Mettre en place la surcharge de l'opérateur = = permettant de tester l'égalité entre deux
nombres complexes.
1 class Complexe
2 {
3 public int reel;
4 public int image;
5 public Complexe(int reel, int image)
6 {
7 [Link]=reel;
8 [Link]=image;
9 }
10 public static Complexe operator *(Complexe x, Complexe y)
11 {
12 int reel=[Link]*[Link]*[Link];
13 int image=[Link]*[Link]+[Link]*[Link];
14 Complexe tmp = new Complexe (reel,image);
15 return tmp;
16 }
17 public static bool operator ==(Complexe x, Complexe y)
18 {
19 if (([Link]==[Link])&&([Link]==[Link]))return true;
20 else return false;
21 }
22 public static bool operator !=(Complexe x, Complexe y)
23 {
24 if (([Link]==[Link])&&([Link]==[Link]))return false;
25 else return true;
26 }
27
28 }
29 class Class1
30 {
31 static void Main(string[] args)
32 {
33 Complexe ca = new Complexe(10,20);
34 Complexe cb = new Complexe(20,30);
35 Complexe cc = ca * cb;
36 [Link]("resultat:{0}+i{1}",[Link],[Link]);
37 if (ca == cb) [Link]("Complexes égaux");
38 else [Link]("Complexes différents");
39 }
41 }

Remarques:

Les lignes 35 et 37 mettent en évidence l'intérêt d'utiliser les opérateurs dans la simplicité de
syntaxe utilisée pour multiplier ou comparer deux objets qui sont ici des instances de ma classe
Complexe.
Les lignes 17-21 comprennent la surcharge de l'opérateur = =. En C++ nous aurions pu choisir
une méthode retournant un entier car le type natif booléen n'existait pas et de ce fait, les types
énumérés BOOL étaient associés à des entiers: l'entier 0 correspondait à une valeur fausse tandis
que l'entier 1 ou différent de 0 était associé à une valeur vraie. En C#, ce n'est plus autorisé: une
instruction if a besoin d'un type bool en paramètre et pas d'un type entier. Il n'existe pas de
conversions implicites entre en entier et un type bool.
Le C# tout comme le C++ demande la surcharge des deux opérateurs = = et !=: l'une ne peut être
réalisée sans l'autre.
Nous reprenons dans le tableau suivant les opérateurs qui peuvent être surchargés:

Catégorie Opérateurs Restrictions


Arithmétique binaire +,*,/,-,% aucune
Arithmétique unaire ++,-- aucune
Bit à bit binaire &,|,^,<<,>> aucune
Bit à bit unaire !,~ aucune
Comparaison = =,!=,>=,<,<=,> surchargée par paire
Certains seront étonnés de ne plus retrouver l'opérateur [] que l'on pouvait surcharger en C++.
Cette impossibilité du C# est compensée par l'utilisation possible des indexeurs.

5.8. Les indexeurs.

Un exemple classique de l'utilisation de la surcharge de l'opérateur [] en C++ était la possibilité de


pouvoir accéder à un élément d'un vecteur étant lui-même un objet d'une classe.
Reprenons cet exemple et adaptons le à la lumière du C#:

1 class vecteur
2 {
3 int [] tab;
4 int taille;
5 public int this [int i]
6 {
7 get
8 {
9 if (i<taille)
10 return tab[i];
11 else
12 throw new IndexOutOfRangeException("Acces");
13 }
14 set
15 {
16 if (i<taille)
17 tab[i]= value;
18 else
19 throw new IndexOutOfRangeException("Acces");
20 }
21 }
22 public vecteur(int taille)
23 {
24 tab = new int[taille];
25 [Link]=taille;
26 }
27 }
28 class Class1
29 {
30 static void Main(string[] args)
31 {
32 vecteur x = new vecteur(10);
33 x[1]=10;
34 x[9]=12;
35 [Link]("x[1]="+x[1]);
36 }
37 }

Remarques:

L'utilisation d'un indexeur est fort semblable à la mise en place des propriétés accessibles par les
accesseurs set et get (get pour la lecture, set pour l'écriture).
Nous retrouvons à la ligne 5 la déclaration de la méthode liée à l'indexeur sous la syntaxe suivante:
public int this [int i] L'opérateur this permet de référencer l'instance ayant servie à utiliser
l'indexeur. Si nous avions envisagé une classe permettant la gestion d'une matrice avec l'accès à un
tableau à deux dimensions, nous aurions eu la syntaxe public int this [int i, int j].
Une particularité aux lignes 12 et 19 est l'emploi d'une syntaxe permettant de générer une
exception qui sera prise en charge par le runtime et indiquant qu'il y a eu une tentative d'accès en
dehors des limites de notre vecteur. Si dans notre exemple, nous essayons d'entrer la ligne de code
suivante: x[10]=20; nous obtiendrons lors de l'exécution le message d'erreur suivant:
Unhandled Exception: [Link]: Acces

5.9. Les constructeurs statiques.

Une nouvelle caractéristique de C# est de permettre l'écriture d'un constructeur statique sans
paramètre. Ce constructeur n'est appelé qu'une seule fois mais le runtime ne garantit pas à quel
moment ce constructeur est exécuté ni l'ordre dans lesquels ils le seront si plusieurs constructeurs
existent dans différentes classes. Nous pouvons simplement dire que le constructeur sera en
général exécuté immédiatement avant le premier appel à un des membres de la classe.
Comme c'est le runtime qui appelle ce constructeur, nous ne retrouverons pas de paramètre ni de
modificateurs d'accès de type public ou private qui n'auront pas de sens.
L'utilisation de tels constructeurs peut se comprendre lorsque l'on doit initialiser des membres
déclarés en static.
Nous retrouverons dans le paragraphe suivant la mise en place d'un tel constructeur.

5.10. Les champs en lecture seule.

Dans la norme ANSI du langage C, il était déjà possible de déclarer une variable en la faisant
précéder du mot clef 'const'. Ce mot clef permettait de déclarer une constante dont l'initialisation
devait se faire lors de la déclaration.
Dans le langage C#, il est possible de déclarer une variable membre d'une classe avec le mot clef
'readonly' permettant de rendre ce membre accessible en lecture seule. L'initialisation d'un tel
membre peut se faire uniquement dans le constructeur pour les membres instanciés et lors de la
déclaration ou dans un constructeur 'static' pour les membres de classe déclarée avec le mot clef
'static'.
Si nous reprenons notre exemple permettant la gestion des nombres complexes, nous pourrions
ajouter un membre statique renseignant le nombre maximum d'instance que l'on autoriserait à
créer.

1 class Complexe
2 {
3 public int reel;
4 public int image;
5 public static readonly uint MaxComplexes;
6 public Complexe(int reel, int image)
7 {
8 [Link]=reel;
9 [Link]=image;
10 }
11 static Complexe ()
12 {
13 MaxComplexes=20;
14 }
15 }
16 class Class1
17 {
18 static void Main(string[] args)
19 {
20 Complexe x = new Complexe(10,20);
21 [Link]([Link]);
22 [Link]=30;
23 }
24 }
Remarques:

A la ligne 5, nous retrouvons un membre de classe déclaré avec le mot clef static, le rendant
accessible en lecture seule. Nous avons toutefois tenté d'y accéder à la ligne 21 dans la méthode
main et nous obtenons une erreur à la compilation:
...[Link](26): A static readonly field cannot be assigned to (except in a static
constructor or a variable initializer)

A la ligne 11, nous retrouvons un constructeur assez particulier puisqu'il est précédé du mot clef
static. C'est ce que nous appelons un constructeur statique dont les caractéristiques essentielles ont
été données lors d'un paragraphe précédent.

5.11. Les classes abstraites, les méthodes abstraites et les interfaces

Certaines classes ne sont pas désignées pour être instanciées. Au contraire, elles sont désignées
pour être simplement héritées d'une classe dérivée pouvant avoir elle-même des instances. Nous
utilisons lors de la déclaration de la classe le mot clef abstract.
Dans une classe, nous pouvons également retrouver des méthodes ayant le modificateur abstract;
celles-ci n'ont pas de définition mais juste une déclaration. Ces classes ne peuvent pas être non
plus dérivées puisque de telles méthodes ne peuvent pas être instanciées.
Une 'interface' est une classe qui ne présentera que des méthodes abstraites. Cependant, de telles
classes ne sont pas déclarées avec le mot clef class mais avec le mot clef interface. Une classe
dérivée ne peut hériter que d'une seule classe de base mais elle peut hériter de plusieurs interfaces.
Une interface définie une sorte de contrat. Une classe ou une structure qui implémente une
interface doit adhérer à ce contrat. Les interfaces peuvent contenir des méthodes, des événements
et des indexeurs comme membres.

Nous mettrons en place différentes classes permettant d'effectuer des mesures de surface et de
périmètre sur des formes géométriques de type parallélépipédiques.
Nous devrons retrouver une classe spécifique pour chaque forme mais nous souhaitons mettre en
place dans chaque classe un nombre de méthodes identiques permettant leur usage. Une façon
simple d'y arriver est d'utiliser une interface.

1 interface formes
2 {
3 double surface();
4 double perimetre();
5 }
6
7 class carre:formes
8 {
9 public double cote;
10 }
11
12 class Class1
13 {
14 static void Main(string[] args)
15 {
16 }
17 }

Remarques:

Nous retrouvons à la ligne 7 la syntaxe permettant à une classe d'hériter d'une interface. Dès que
cet héritage est mis en place, il est indispensable de déclarer une méthode surface() et perimetre ()
dans la classe carre faute de quoi, le compilateur nous donne les erreurs suivantes:
[Link](14,8): error CS0535: '[Link]' does not implement interface
member '[Link]()'
[Link](14,8): error CS0535: '[Link]' does not implement interface
member '[Link]()'

Modifions notre exemple de sorte de faire apparaître les méthodes souhaitées:

1 interface Iformes
2 {
3 double surface();
4 double perimetre();
5 }
6
7 class carre:Iformes
8 {
9 double cote;
10 public carre(double cote)
11 {
12 [Link]=cote;
13 }
14 public double surface()
15 {
16 return (cote*cote);
17 }
18 public double perimetre()
19 {
20 return (cote*4);
21 }
22 }
23
24 class Class1
25 {
26 static void Main(string[] args)
27 {
28 carre ca = new carre(10.5);
29 [Link]([Link]());
30 perimetre(ca);
31
32 }
33 static void perimetre(Iformes x)
34 {
35 [Link]([Link]());
36 }
37
38 }

Remarques:

Une des caractéristiques des interfaces est que l'on peut créer des références de ces interfaces
capables de pointer vers n'importe quelle instance d'une classe héritant de cette instance. La ligne
33 comprend un passage par référence sur l'interface Iformes.
On peut faire en sorte que certaines méthodes ne puissent être appelées que par une référence de
l'interface. Il suffit pour cela de déclarer la méthode dans la classe en utilisant la syntaxe suivante:
la ligne 14 doit par exemple être remplacée par double [Link](). Si rien d'autre n'est
changé au code, le compilateur génère une erreur à la ligne 29 car la méthode surface ne peut plus
être appelée que par une référence de l'interface. Voici ce que la méthode main() doit contenir
après correction:
static void Main(string[] args)
{
carre ca = new carre(10.5);
Iformes test=ca;
[Link]([Link]());
perimetre(ca);
}

5.12. Les appels de constructeurs.

Nous avons vu à plusieurs reprises à travers les différents exercices la façon dont les différents
constructeurs sont appelés.
Par défaut, un constructeur normal est appelé à chaque création d'une instance de la classe. Si
plusieurs constructeurs existent, les appels suivent les mêmes règles que pour les appels des
méthodes surchargées. Pour le constructeur statique, l'exécution est effectuée avant le premier
appel à un des membres de la classe.
On ne peut appeler explicitement un constructeur que dans un des cas suivants:
- appel au constructeur de la classe de base dont on hérite.
- appel à un autre constructeur de la même classe
Reprenons l'exemple que l'on a traité sur la classe carre héritant de la classe rectangle:

4 class Rectangle
5 {
6 protected float longueur;
7 protected float largeur;
8 public Rectangle(float largeur, float longueur)
9 {
10 [Link] = largeur;
11 [Link] = longueur;
12 }
13 public float surface()
14 {
15 return largeur*longueur;
16 }
17 }
18 class Carre:Rectangle
19 {
20 public Carre(float largeur):base(largeur,0)
21 {
22
23 }
24 public float surface()
25 {
26 return largeur*largeur;
27 }
28 }

Remarques:

Nous retrouvons à la ligne 20 l'appel à un constructeur de la classe de base sous la forme


public Carre(float largeur):base(largeur,0)
Comme le langage C# prévoit l'héritage d'une seule classe de base, l'appel à son constructeur a été
fortement simplifié puisque nous retrouvons base (largeur,0).
Si dans notre exemple nous créons un constructeur par défaut dans la classe 'carre', nous pourrons
retrouver un appel de constructeur sous la forme suivante:

1 public Carre(float largeur):base(largeur,0)


2 {
3
4 }
5 public Carre():this(10)
6 {
7 }

Nous retrouverons donc uniquement base ou this comme mots clefs autorisés pour l'appel à un
autre constructeur. Tout manquement à cette règle provoquerait une erreur lors de la compilation.

5.13. Les destructeurs et Dispose().

Pour bien comprendre l'utilité d'un destructeur, il est important de se fixer de nouveau les idées sur
la façon dont les objets sont créés et détruits sous .NET.
La seule façon d'instancier une classe, c-à-d de créer un nouvel objet est d'utiliser l'opérateur new
qui se présente sous la syntaxe suivante:
classe x = new classe ( ); classe doit être remplacé par le nom de la classe que nous souhaitons
instancier et les parenthèses peuvent contenir des paramètres suivant les constructeurs que nous
désirons implicitement appeler si nous les avons créés dans notre classe.
Dans le langage C++, un objet pouvait être créé sans utiliser l'opérateur new. Celui-ci était détruit
en fonction du fait que ce soit un objet local défini dans une fonction ou un bloc d'instructions ou
un objet global défini avant la fonction main. Un objet local était détruit lorsque l'on quittait la
fonction ou le bloc et un objet global détruit à la sortie du programme. Pour rappel, un objet local
était créé sur la pile tandis qu'un objet global sur le tas.
Dans le langage C++, l'opérateur new permettait de réaliser de l'allocation dynamique, celui-ci
étant lié à l'opérateur delete permettant de libérer l'espace mémoire occupé par l'objet.
Dans le langage C#, la création d'un objet doit se faire obligatoirement par l'opérateur new. Celui-
ci permet de créer un objet sur le tas même si l'utilisation de cet opérateur s'effectue dans une
fonction. Il n'existe pas d'opérateur delete.
Tout comme pour l'opérateur new en C++ qui nous obligeait à déclarer un pointeur capable de
contenir l'adresse renvoyée, le C# nous oblige à utiliser une référence capable de référencer l'objet
créé. Tandis que la référence sera placée sur la pile, l'objet est placé sur le tas.
Lorsque un objet n'est plus référencé, ce n'est plus le programmeur qui libère l'espace mémoire
occupé par cet objet mais c'est un des éléments du runtime que l'on appelle Garbage Collector
(ramasse miettes). Le rôle de ce ramasse miettes est d'assurer la gestion de la mémoire et d'enlever
le cas échéant des objets n'étant plus référencés.

Pour bien comprendre ce concept, reprenons l'exemple suivant:

1 class complexe
2 {
3 float reel;
4 float image;
5 public static int compteur=0;
6 public complexe(float reel, float image)
7 {
8 [Link]=reel;
9 [Link]=image;
10 compteur++;
11 }
12 ~complexe()
13 {
14 compteur --;
15 }
16 }
17
18 class Class1
19 {
20 static void Main(string[] args)
21 {
22 complexe x = new complexe(1.2F,2.5F);
23 complexe y = new complexe(1.2F,2.5F);
24 creation();
25 }
26 static void creation()
27 {
28 complexe a = new complexe(10.0F,15.0F);
29 complexe b = new complexe(5.0F,4.5F);
30 complexe c = new complexe(1.1F,2.2F);
31 }
32 }

L'idée générale de ce code est d'utiliser un compteur d'instance qui est un membre statique de la
classe; ce compteur est incrémenté dans le constructeur et décrémenté dans le destructeur. Le
destructeur est une méthode analogue au constructeur mais appelée implicitement lors de la
destruction de l'objet. Elle porte le même nom que celui de la classe, n'a pas de type de retour ni
de paramètre.
Dans la fonction creation ( ), nous créons trois objets de la classe complexe et nous initialisons
trois références en correspondance avec chacun de ces objets; les références étant créées sur la
pile, elles seront détruites lorsque l'on sortira de la fonction.
Dans la fonction Main, nous créons deux objets et ensuite nous appelons la fonction creation ( ).
La question que l'on peut se poser est de savoir combien d'objet resteront présents sur le tas
lorsque, après l'appel à la fonction creation ( ), on affichera la valeur du membre statique
compteur. Comme c'est le GC qui s'occupe de gérer le tas, il y a de forte chance qu'aucun objet ne
sera encore détruit malgré le fait que parmi les cinq, trois de ces objets ne sont plus référencés.
Si nous ajoutons après la ligne 24 un affichage à l'écran du contenu du membre compteur, nous
aurons la valeur 5.

Question: comment peut on forcer le GC à nettoyer le tas? Il existe en fait une seule solution qui
consiste à utiliser la méthode Collect( ). En fait, cet appel ne doit pas être systématique car il
demande des ressources supplémentaires au niveau de votre ordinateur. Le point le plus important
est la gestion des ressources nom managés tels que par exemple les accès à une base de données.
On pourrait supposer placer la libération de telles ressources dans le destructeur mais le
destructeur n'est appeler que lors de la destruction de l'objet qui est sous la responsabilité du GC
et qui se produira naturellement à un moment non prévisible par le programmeur et bien souvent
après que l'on ait quitté l'application. Pour forcer la gestion des ressources non managés, deux
solutions sont envisageables:

• Nettoyage complet du tas managé: il existe une classe GC appartenant à l'espace de nom
System et comprenant la méthode statique Collect( ). Comme la destruction d'un objet
managé provoque l'appel du destructeur, on placera dans ce dernier la libération des
ressources non managées par le GC.
• Nettoyage sélectif des ressources non managées liées à un objet managé. Cette possibilité
est offerte par l'utilisation de l'interface IDisposable qui nous oblige à placer dans les
classes dérivées la méthode Dispose( ).

a) Reprenons le code précédemment développé dans lequel nous utilisons la méthode statique
Collect().

1 static void Main(string[] args)


2 {
3 complexe x = new complexe(1.2F,2.5F);
4 complexe y = new complexe(1.2F,2.5F);
5 creation();
6 [Link]([Link]);
7 [Link]();
8 [Link]();
9 [Link]([Link]);
10 }

La ligne 7 permet de demander au GC le nettoyage du tas managé. La ligne 8 permet de mettre


votre programme artificiellement en attente pour le nettoyage puisse avoir lieu et la ligne 9
provoquera l'affichage du membre compteur qui contient alors la valeur 2. En effet les trois objets
créés dans la fonction ne sont plus référencés et ont donc été supprimés du tas par le GC.
Le fait que le compteur se décrémente est bien la preuve que le destructeur est appelé
implicitement par le GC lors de la destruction des objets du tas managé.

b) Faisons maintenant hériter notre classe complexe de l'interface IDiposable. Cette dérivation
nous oblige à implémenter la méthode Dispose( ) qui pourra être appelée par l' objet dont la
suppression nécessiterait la gestion de ressources non managées. Qu'en est il de l'objet en lui-
même? Est il détruit par lors de l'appel de Dispose( )?

1 class complexe:IDisposable
2 {
3 public float reel;
4 public float image;
5 public static int compteur=0;
6 public complexe(float reel, float image)
7 {
8 [Link]=reel;
9 [Link]=image;
10 compteur++;
11 }
12 public void Dispose()
13 {
14
15 }
16 ~complexe()
17 {
18 compteur --;
19 }
20 }
21
22 class Class1
23 {
24 static void Main(string[] args)
25 {
26 complexe x = new complexe(1.2F,2.5F);
27 complexe y = new complexe(1.2F,2.5F);
28 creation();
29 [Link]();
30 [Link]([Link]);
31 }
32 static void creation()
33 {
34 complexe a = new complexe(10.0F,15.0F);
35 complexe b = new complexe(5.0F,4.5F);
36 complexe c = new complexe(1.1F,2.2F);
37 }
38 }
L'exécution d'un tel code donne comme affichage 5 c-à-d que le GC n'a pas détruit l'objet puisque
le destructeur n'a pas été appelé. Pour s'en convaincre, il suffit d'essayer d'accéder à cet objet
après l'appel à la méthode Dispose( ) en ajoutant la ligne de code [Link]( [Link]);
L'exécution du code ainsi modifié donnera l'affichage 1.2 pour la partie réelle du complexe y.
L'objet n'est donc pas détruit.
A quoi peut donc servir Dispose( )? A simplement libérer manuellement les ressources non
managés tandis que les ressources managés sont du ressort du GC.
La situation est donc la suivante:

• La méthode Dispose( ) est appelée explicitement dans le code et jamais par le GC. Cette
méthode doit comprendre la libération des ressources non managés.
• Le destructeur est appelé implicitement par le GC. Ne faut il pas prévoir également la
libération des ressources non managées dans le cas où nous oublierions d'appeler la
méthode Dispose( ). Dans ce cas de figure, n'y a-t-il pas un risque que ces méthodes
soient appelées toutes les deux? Pour éviter ce problème, nous devrons implémenter dans
la méthode Dispose() l'appel à la méthode SuppressFinalize( this ) permettant de
renseigner au GC que lors de la suppression de l'objet du tas managé, le destructeur ne
doit plus être appelé du fait que les ressources non managées ont été libérées.

Voici à quoi devrait donc ressembler notre code:

1 class complexe:IDisposable
2 {
3 float reel;
4 float image;
5 public static int compteur=0;
6 public complexe(float reel, float image)
7 {
8 [Link]=reel;
9 [Link]=image;
10 compteur++;
11 }
12 public void Dispose()
13 {
14 Dispose(true);
15 [Link](this);
16
17 }
18 protected virtual void Dispose(bool disposing)
19 {
20 //libération des ressources non managées.
21 }
22 ~complexe()
23 {
24 Dispose (false);
25 compteur --;
26 }
27 }

Nous avons créé une méthode Dispose (bool disposing) comprenant la libération des méthodes
non managées et étant appelable à la fois du destructeur et de la méthode dispose.
Nous remarquons à la ligne 15 l'emploi de la méthode [Link](this) permettant de
renseigner au GC que, dans ce cas, comme les ressources non managées ont été libérées, il n'est
plus nécessaire d'appeler le destructeur lors de la destruction de l'objet sur le tas managé.
Pour s'en convaincre, il suffit de modifier le contenu de la fonction creation ( ) en y incluant les
lignes de code suivantes:
1 static void creation()
2 {
3 complexe a = new complexe(10.0F,15.0F);
4 complexe b = new complexe(5.0F,4.5F);
5 complexe c = new complexe(1.1F,2.2F);
6 [Link]();
7 [Link]();
8 }

Si nous appelons la méthode [Link]( ) dans la fonction Main( ), que va-t-il se passer lors de
l'affichage du membre compteur qui n'est que décrémenté dans le destructeur? Voici le contenu
de la fonction Main( ):

1 static void Main(string[] args)


2 {
3 complexe x = new complexe(1.2F,2.5F);
4 complexe y = new complexe(1.2F,2.5F);
5 creation();
6 [Link]();
7 [Link]();
8 [Link]([Link]);
9 }
10

Si nous comptons le nombre d'objets créés et le nombre d'objets que le GC va détruire, nous
obtenons:
5 objets créés: 2 dans le Main( ) et 3 dans la fonction creation ( ).
3 objets détruits puisque les 3 objets créés dans la fonction creation ( ) ne sont plus référencés.
Malgré tout, l'affichage du membre compteur donne la valeur 4. En effet, pour les références c et
b, la méthode Dispose( ) a été appelée et de ce fait SuppressFinalize( ) également. Le GC n'a donc
pas appelé le destructeur des objets associés aux références c et b.

Nous pouvons finalement aborder l'aspect suivant: que se passe t il si un des membres de la classe
est lui même un objet d'une autre classe présentant elle même une méthode Dispose()? Cette
méthode doit être appelée explicitement lors de l'appel à notre propre méthode Dispose() mais pas
lorsqu'il s'agit de l'appel de notre destructeur puisque cet objet est lui même managé et que donc
son destructeur sera appelé. Nous obtiendrons alors le code suivant:

1 protected virtual void Dispose(bool disposing)


2 {
3 if (disposing == true)
4 {
5 //appel d'autres méthodes Dispose( )
6 //d'objet managés appartenant à la classe
7 }
8 //libération des ressources non managées.
9 }

Nous ajouterons simplement que comme la méthode Dispose( ) peut être appelée à plusieurs
reprises, l'objet n'étant pas été détruit, il est important d'ajouter un booléen comme membre de la
classe permettant ainsi d'effectuer un test. Voici le code:

1 class complexe:Idisposable
2 {
3 float reel;
4 float image;
5 bool disposed;
6 public static int compteur=0;
7 public complexe(float reel, float image)
8 {
9 [Link]=reel;
10 [Link]=image;
11 compteur++;
12 disposed=false;
13 }
14 public void Dispose()
15 {
16 if (!disposed)
17 {
18 Dispose(true);
19 [Link](this);
20 disposed=true;
21 }
22 }
23 protected virtual void Dispose(bool disposing)
24 {
25 //libération des ressources non managées.
26 }
27 ~complexe()
28 {
29 Dispose (false);
30 compteur --;
31 }
32 }

Reprenons un exemple complet dans lequel nous utiliserons notamment une classe Image
proposant sa méthode Dispose( ). Il sera intéressant de disposer de l'analyseur de performances du
gestionnaire de tâches pour voir comment évolue la mémoire

1 class GestionImage:IDisposable
2 {
3 Image picture = null;
4 protected bool disposed=false;
5 public string Picture
6 {
7 set
8 {
9 picture=[Link](value);
10 }
11 }
12 public GestionImage()
13 {
14 [Link]("appel constructeur");
15 }
16 public void Dispose()
17 {
18 [Link] ("Appel explicite de libération");
19 Dispose(true);
20 [Link](this);
21
22 }
23 protected virtual void Dispose(bool disposing)
24 {
25 if (!disposed)
26 {
27 [Link]("ressources pas encore
libérées");
28 if (disposing == true)
29 {
30 [Link]("libération ressources
managées");
31 if (picture != null)
32 {
33 [Link]();
34 picture=null;
35 }
36 }
37 [Link]("libération ressources non
managées");
38 disposed=true;
39 }
40 else
41 {
42 [Link]("ressources déjà libérées");
43 }
44 }
45 ~GestionImage()
46 {
47 [Link]("appel destructeur");
48 Dispose (false);
49 }
50 }
51
52 class Class1
53 {
54 static void Main(string[] args)
55 {
56 GestionImage temp1=new GestionImage();
57 [Link]="c:\\[Link]";
58 GestionImage temp2=new GestionImage();
59 [Link]="c:\\[Link]";
60 GestionImage temp3=new GestionImage();
61 [Link]="c:\\[Link]";
62 [Link]();
63 [Link]();
64 [Link]();
65 [Link]("fin du programme");
66 }
67
68 }

De la ligne 56 à la ligne 61, nous allons créer des objets de type image. Le fichier qui nous sert en
test fait 656Ko, ce qui nous permet d'avoir un impact sur les jauges présentées dans l'analyseur de
performances. L'évolution des jauges n'est pas nécessairement proportionnelle à la taille du
fichier. Avant toute exécution du code, l'utilisation du fichier d'échange était de 223Mo. Lorsque
nous arrivons au premier Readline( ) , nous obtenons la valeur suivante: 246Mo.
Au deuxième ReadLine( ), nous obtenons 240Mo et à la sortie du programme, 223Mo.
L'affichage après exécution est le suivant:

appel constructeur
appel constructeur
appel constructeur

Appel explicite de libération


ressources pas encore libérées
libération ressources managées
libération ressources non managées
fin du programme
appel destructeur
ressources pas encore libérées
libération ressources non managées
appel destructeur
ressources pas encore libérées
libération ressources non managées

5.14. Les délégués.

Dans le langage C et C++ nous avions la possibilité de déclarer des pointeurs de fonctions
permettant de contenir une adresse correspondant à une fonction à exécuter. En C#, les pointeurs
de fonctions ont été remplacés par les délégués. Ils interviennent lorsque l'on désire passer des
méthodes à d'autres méthodes. Nous pouvons penser à la méthode de la classe thread permettant
de démarrer un thread et dont la méthode [Link]( ) a besoin d'un paramètre correspondant à
la méthode qui doit être invoquée par le thread.
On peut voir les délégués comme étant un nouveau type en C#. En effet, l'utilisation des délégués
nécessite une déclaration de type, la création de références et des instanciations.

Pour bien comprendre l'utilisation de références, nous allons considérer l'exemple suivant: soit une
méthode devant assurer le tri d'un tableau d'entiers suivant la méthode du tri à bulles.

1 class Class1
2 {
3 static void Main(string[] args)
4 {
5 int [] tab = {2,7,1,4,3,8,6,5,10,9};
6 tri(tab);
7 for (int i=0; i<[Link];i++)
8 {
9 [Link](tab[i]);
10 }
11 }
12 static void tri(int [] tab)
13 {
14 for (int i=0; i< [Link];i++)
15 {
16 for (int j=i+1;j<[Link];j++)
17 {
18 if (tab[j]<tab[i])
19 {
20 int temp = tab[i];
21 tab[i]=tab[j];
22 tab[j]=temp;
23 }
24 }
25 }
26 }
27 }

Imaginons maintenant que l'on désire trier un tableau d'objets sans avoir à réécrire la
fonction assurant le tri. Nous devons naturellement changer les éléments suivants dans la
fonction:
• A la ligne 18, nous retrouvons une comparaison d'entiers. Il faudrait remplacer le
code par une comparaison d'objets qu l'on désire trier.
• Aux lignes 20, 21 et 22 nous permutons deux entiers; il faut à cet endroit modifier
le code pour que l'opération porte sur des objets.
L'idée est de remplacer l'opérateur de comparaison par une méthode capable de comparer
deux des objets que l'on désire trier et de renvoyer un booléen. Pour que nous puissions
utiliser n'importe quel type d'objets, l'idée est de pouvoir passer la méthode en paramètre,
méthode adaptée aux objets à trier. C'est dans ce cadre que l'utilisation des délégués est
intéressante.

La déclaration du type délégué sera sous la forme suivante:

delegate bool Compare(Object x, Object y);

Delegate Mot clef permettant de déclarer un type délégué.


Bool C'est le type de retour de la fonction vers laquelle le l'instance
du délégué pointera.
Compare Nom du type délégué.
(object x, object y) Liste des paramètres de la fonction vers laquelle l'instance du
délégué pointera. (*)

(*) Sachant qu'en C# tout est classe, que ces classes héritent d'une classe de base qui est
object et que toute instance d'une classe de base peut pointer vers un objet d'une classe
dérivée, on comprend que si une instance du délégué 'Compare' peut pointer vers
n'importe quelle méthode capable de trier un type d'objet donné, on doit utiliser comme
type de paramètre une référence à la classe de base object.

1 delegate bool Compare(object x, object y);


2
3 class Class1
4 {
5 static void Main(string[] args)
6 {
7 int [] tab = {2,7,1,4,3,8,6,5,10,9};
8 Compare comp = new Compare(CompInt);
9 tri(tab,comp);
10 for (int i=0; i<[Link];i++)
11 {
12 [Link](tab[i]);
13 }
14 }
15 static void tri(int [] tab,Compare comp)
16 {
17 for (int i=0; i< [Link];i++)
18 {
19 for (int j=i+1;j<[Link];j++)
20 {
21 if (comp(tab[j],tab[i]))
22 {
23 int temp = tab[i];
24 tab[i]=tab[j];
25 tab[j]=temp;
26 }
27 }
28 }
29 }
30 static bool CompInt(object x, object y)
31 {
32 return ((int)x<(int)y);
33 }
34 }

Commentaires:

A partir de la ligne 30, nous retrouvons la méthode capable de comparer deux entiers. Il
est important de conserver des paramètres de type object pour rester en conformité avec
la référence du type délégué 'Compare' avec lequel nous travaillerons. A la ligne 20, nous
retrouvons la comparaison des deux entiers en nous obligeant à effectuer un transtypage
entre les références de la classe de base et les entiers correspondant au type natif de
l'objet à comparer.
A la ligne 8, nous retrouvons la créations d'une référence sur le type délégué Compare et
la créations d'une instance de ce délégué par l'opérateur new et le passage en paramètre
du nom de la méthode vers laquelle la référence doit pointer.
A la ligne 15, nous faisons passer une fonction en paramètre par l'intermédiaire d'une
référence au délégué Compare.

Bien que cet exemple soit fonctionnel, il se limite à des tableaux d'entiers puisque la
fonction de tri comprend encore comme premier paramètre une référence sur un tableau
d'entiers et la permutation se base encore sur une variable temporaire qui est de type
entier. Nous allons adapter notre code sur base de nos propres objets correspondant à une
classe client.

1 class client
2 {
3 string Nom;
4 double dettes;
5 public double Dettes
6 {
7 Set
8 {
9 dettes=value;
10 }
11 Get
12 {
13 return dettes;
14 }
15 }
16 public client(string Nom, double dettes)
17 {
18 [Link]=Nom;
19 [Link]=dettes;
20 }
21 public client(string Nom):this(Nom,0.0)
22 {
23 }
24 }

Le principe de cette classe repose sur la gestion des ardoises au bar des étudiants. Il serait
intéressant de pouvoir trier un tableau d'objets de cette classe sur base du montant des
dettes. Nous allons donc adapter la méthode de tri sous la forme suivante:
1 static bool CompClient(object x, object y)
2 {
3 Client tmp1= (Client)x;
4 Client tmp2= (Client)y;
5 return ([Link]<[Link]);
6 }

Reprenons maintenant l'ensemble du code modifié.

1 delegate bool Compare(object x, object y);


2 class Client
3 {
4 public string Nom;
5 double dettes;
6 public double Dettes
7 {
8 Set
9 {
10 dettes=value;
11 }
12 Get
13 {
14 return dettes;
15 }
16 }
17 public Client(string Nom, double dettes)
18 {
19 [Link]=Nom;
20 [Link]=dettes;
21 }
22 public Client(string Nom):this(Nom,0.0)
23 {
24 }
25 }
26 class Class1
27 {
27 static void Main(string[] args)
28 {
29
30 Compare comp = new Compare(CompClient);
31 Client [] tab={
32 new Client("Dupond",50.45),
33 new Client("Dubois",10.0),
34 new Client("Dupont",15.50)};
35 tri(tab ,comp);
36 for (int i=0; i<[Link];i++)
37 {
38 [Link]("Le client {0} a {1} euros de
dettes",tab[i].Nom,tab[i].Dettes);
39 }
40 }
41 static void tri(object[] tab,Compare comp)
42 {
43 for (int i=0; i< [Link];i++)
44 {
45 for (int j=i+1;j<[Link];j++)
46 {
47 if (comp(tab[j],tab[i]))
48 {
49 object temp = tab[i];
50 tab[i]=tab[j];
51 tab[j]=temp;
52 }
53 }
54 }
55 }
56 static bool CompClient(object x, object y)
57 {
58 Client tmp1= (Client)x;
59 Client tmp2= (Client)y;
60 return ([Link]<[Link]);
61 }
62
63 }

L'exécution du code donne l'affichage suivant:

Le client Dubois a 10 euros de dettes


Le client Dupont a 15,5 euros de dettes
Le client Dupond a 50,45 euros de dettes

Commentaires:

A la ligne 49, nous avons modifié la permutation qui s'effectue maintenant sur base
d'objets de la classe object et non plus d'objets de type entiers.
A la ligne 41, le premier paramètre est un tableau d'objets de type object et non plus un
tableau d'entiers.

5.15. Le transtypage défini par l'utilisateur.

Le C# autorise deux types de transtypage: celui implicite et celui explicite. Dans le cas
d'un transtypage explicite, celui-ci est marqué explicitement dans le code par le type de
données de destination entre parenthèses.

Exemple:

int a = 10;
long b = a; // transtypage implicite
short c = (short) a; //transtypage explicite.

Pour nos propres classes, nous pouvons définir des transtypage de façon analogue à la
surcharge des opérateurs en utilisant les mots clefs implicit ou explicit. Reprenons notre
exemple du paragraphe précédent en nous intéressant à la classe Client. Nous pourrions
envisager la syntaxe suivante où l'on convertirait un objet de cette classe vers un entier
qui correspondrait par exemple à ses dettes.

Client x = new x ("Dupond");


int dette = (int) x;

1 delegate bool Compare(object x, object y);


2 class Client
3 {
4 public string Nom;
5 double dettes;
6 public static explicit operator double(Client x)
7 {
8 return [Link];
9 }
10 public double Dettes
11 {
12 Set
13 {
14 dettes=value;
15 }
16 Get
17 {
18 return dettes;
19 }
20 }
21 public Client(string Nom, double dettes)
22 {
23 [Link]=Nom;
24 [Link]=dettes;
25 }
26 public Client(string Nom):this(Nom,0.0)
27 {
28 }
29 }
30 class Class1
31 {
32 static void Main(string[] args)
33 {
34 Client x = new Client("Dupond",10.50);
35 double dette = (double)x;
36 [Link]("Le client {0} a {1} euros de
dettes",[Link],(double)x);
37 }
38
39 }

Commentaires:

La ligne 6 reprend la syntaxe permettant la mise en place de notre transtypage explicite:


public static explicit operator double(Client x)
public static explicit operator sont les mots clefs,
double indique le type résultant du transtypage
Client x indique le type et la référence devant être transtypés.

6. La gestion des erreurs et des exceptions.

6.1. Introduction.

Jusqu'à présent, les différentes exceptions qui étaient levées dans nos codes d'exemples
étaient récupérées par le système d'exploitation et provoquaient l'arrêt de notre programme
avec l'affichage d'un message d'erreur. Il est plus adéquat de capturer autant que possible
ces exceptions afin de contrôler le bon déroulement de votre application. Vous pouvez
également générer des exceptions par les classes mises à votre disposition héritant de
[Link] mais également créer vos propres classes d'exception par la dérivation
de ApplicationException.

6.2. Capture d'exception.

La capture des exceptions en C# peut s'effectuer en subdivisant la partie de votre code en


trois blocs caractérisés par les mots clefs try, catch et finally.

• Le bloc try correspond au bloc qui contient les opérations normales pouvant être à
l'origine de l'erreur.
• Le bloc catch correspond au bloc qui contient le code à exécuter en cas d'erreur.
• Le bloc finally correspond au bloc contenant le code permettant de nettoyer les
ressources ou d'effectuer toute action qu vous souhaitez effectuer à la fin d'un bloc
try ou catch. Le contenu du bloc finally est exécuté qu'il y ait une condition
d'erreur ou pas. Ce bloc est optionnel.

Considérons l'exemple d'une division par zéro.


1 class Class1
2 {
3 static void Main(string[] args)
4 {
5 int a=20;
6 int b=0,c;
7 Try
8 {
9 c=a/b;
10 }
11 catch([Link] e)
12 {
13 [Link]("Divison par zéro");
14 }
15 [Link]("Fin du programme");
16 }
17
18 }

Nous pouvons spécifier des blocs catch multiples pour permettre des types différents
d'exceptions. Une complication peut surgir du fait que les exceptions forment une
hiérarchie d'objets et de ce fait, une exception particulière peut trouver une
correspondance avec plus qu'un seul bloc catch. Etant donné que c'est le premier bloc
catch rencontré correspondant à l'exception généré qui est exécuté, il est important de
placer en premier les blocs catch correspondant à des exceptions plus spécifiques et
ensuite de placer les blocs catch correspondant aux exceptions plus générales.
Nous pouvons également générer les exceptions avec l'emploi de throw. Reprenons le
code précédent et adaptons-le à la lumière de ce qui vient d'être dit.

1 static void Main(string[] args)


2 {
3 int a=20;
4 int b=0,c;
5 Try
6 {
7 if (b == 0) throw new
DivideByZeroException("division par zéro");
8 else c=a/b;
9 }
10 catch([Link] e)
11 {
12 [Link]("Cause d'exception: "+[Link]);
13 }
14 catch ([Link] e)
15 {
16 [Link]("Autre exception: "+[Link]);
17 }
18 [Link]("Fin du programme");
19 }

Commentaires:

La ligne 14 comprend un bloc catch qui permettra d'intercepter les autres exceptions qui
ne sont pas prises en charge par un bloc catch spécifique précédent.
La ligne 7 permet de générer soi même une exception dans le code.
Les lignes 12 et 14 mettent en évidence l'accès à une propriété de la classe de base
Exception qui est Message permettant d'afficher le texte natif qui décrit la condition
d'erreur. Nous pouvons retrouver les autres propriétés suivantes:

HelpLink Lien à un fichier d'aide fournissant plus d'informations sur


l'exception
Source Nom de l'application ou de l'objet ayant causé l'exception
StackTrace Détails des appels de méthodes sur la pile (pour faciliter la trace de
la méthode qui a levé l'exception)
TargetSite Un objet de réflexion .NET décrivant la méthode qui a levé
l'exception
InnerException Si cette exception est levée dans un bloc catch, elle contient l'objet
"exception" ayant envoyé le code dans ce bloc catch

StackTrace et TargetSite sont fournis automatiquement par le runtime .NET si une trace
de la pile est disponible. Source est toujours remplie par le runtime .NET avec le nom de
l'assemblage dans lequel l'exception a été levée (nous pouvons en modifier le contenu).
Message, HelpLink et InnerException doivent être remplis par le code qui a levé
l'exception.

6.3. Classes d'exceptions définies par l'utilisateur.

Une classe d'exception définie par l'utilisateur doit dériver de la classe de base
ApplicationException. Imaginons que l'on désire demander l'introduction au clavier d'un
nombre compris entre 0 et 255 et que le non respect de cette restriction génère une
exception que nous avons créée.

1 class Class1
2 {
3 static void Main(string[] args)
4 {
5 int valeur;
6 string Valeur;
7 Try
8 {
9 [Link]("Un nombre entre 0 et 255: ");
10 Valeur = [Link]();
11 valeur = Convert.ToInt32(Valeur);
12 if ((valeur<0)||(valeur>255))
13 throw new OutOfRange("Valeur hors
limite");
14 }
15 catch (OutOfRange e)
16 {
17 [Link]([Link]);
18 }
19 catch([Link] e)
20 {
21 [Link]("Cause d'exception:
"+[Link]);
22 [Link]([Link]);
23 }
24 catch ([Link] e)
25 {
26 [Link]("Autre exception:
"+[Link]);
27 }
28 [Link]("Fin du programme");
29 }
30 }
31 class OutOfRange:ApplicationException
32 {
33 public OutOfRange(string Message):base(Message)
34 {
35 }
36 public OutOfRange(string Message, Exception
InnerException):base(Message,InnerException)
37 {
38 }
39 }

Commentaires:

A partir de la ligne 31, nous retrouvons la création de notre propre exception sous la
forme d'une classe héritant de la classe de base ApplicationException. Dans cette classe,
nous implémentons deux constructeurs chacun comprenant le message qui devra être
fourni en cas d'erreur.

7. Les méthodes anonymes (c# 2.0).

Pour bien comprendre l’utilité des méthodes anonymes, nous allons reprendre l’exemple
traitant de l’utilisation des délégués.

1 delegate bool Compare(object x, object y);


2
3 class Class1
4 {
5 static void Main(string[] args)
6 {
7 int [] tab = {2,7,1,4,3,8,6,5,10,9};
8 Compare comp = new Compare(CompInt);
9 tri(tab,comp);
10 for (int i=0; i<[Link];i++)
11 {
12 [Link](tab[i]);
13 }
14 }
15 static void tri(int [] tab,Compare comp)
16 {
17 for (int i=0; i< [Link];i++)
18 {
19 for (int j=i+1;j<[Link];j++)
20 {
21 if (comp(tab[j],tab[i]))
22 {
23 int temp = tab[i];
24 tab[i]=tab[j];
25 tab[j]=temp;
26 }
27 }
28 }
29 }
30 static bool CompInt(object x, object y)
31 {
32 return ((int)x<(int)y);
33 }
34 }

Pour rappel, l’utilisation d’un délégué nous oblige à respecter trois étapes bien distinctes :

1- La création d’un type délégué.


delegate bool Compare(object x, object y);

2- La création de la fonction.
static bool CompInt(object x, object y)
{
return ((int)x<(int)y);
}

3- L’instanciation du type délégué avec le passage de la fonction en paramètre.

Compare comp = new Compare(CompInt);

L’utilisation des méthodes anonymes nous permet de pouvoir déclarer la fonction en même
temps que le délégué de la façon suivante :

1- delegate bool Compare(object x, object y);

2- Compare comp = delegate (object x, object y)


{
return ((int)x<(int)y) ;
};
Dans cet exemple précédent, nous remarquons que nous avons respecté la signature du
délégué lors de la création de la méthode anonyme, comme cela devait être le cas pour
l’utilisation des délégués en C#1.0. Ce n’est plus nécessaire actuellement et nous pouvons
admettre toute méthode anonyme sous les règles de compatibilité suivantes.
Paramètres :
• La méthode anonyme n’a pas de liste de paramètres et le délégué n’a pas de
paramètres déclarés out.
• La liste de paramètres de la méthode anonyme est la même que celle du délégué.
Type de retour :
• Si le délégué retourne void, alors la méthode anonyme ne doit pas contenir
l’instruction return, ou seulement l’instruction return sans expression derrière.
• Sinon, les expressions retournées doivent pouvoir être implicitement convertie dans le
type de retour du délégué.

Nous pourrions dons imaginer l’exemple suivant :

1 delegate void Message(string chaine);


2
3 Message test1 = delegate () {[Link](“bonjour”)};
4
5 Message test2 = delegate(string MaChaine)
6 {
7 [Link](MaChaine);
8 }
9
10 Message test1 = delegate {[Link](“bonjour”)};
11

En ayant créé un seul type délégué, nous pouvons référencer plusieurs méthodes anonymes
différentes. C’est pratique dans la gestion des événements en programmation Windows Forms
lorsque nous ne souhaitons pas utiliser les paramètres. Si les parenthèses sont omises, la
méthode peut être assignée à n’importe quelle signature.

Une méthode anonyme peut utiliser n’importe quelle variable membre de la classe et elle peut
aussi utiliser toute variable locale définie dans la portée de la méthode qui la contient comme
si c’était sa propre variable locale. Prenons l’exemple suivant tiré de la documentation :

1 delegate int myDelegate();


2
3 class Test
4 {
5 static mydelegate myFunc() //function retournant un délégué
6 {
7 int x=0;
8 myDelegate result = delegate { return ++x;}
9 return result;
10 }
11
12
13
14 static void Main()
15 {
16 myDelegate d = myFunc(); //inférence
17 [Link](d());
18 [Link](d());
19 [Link](d());
20 }
33 }
34

Le résultat de l’exécution du code est 1 2 3, ce qui est assez surprenant si l’on part du principe
qu’une variable locale est détruite lorsque l’on sort de la fonction. Pour expliquer cela, il faut
évoquer la « durée de vie » de la variable qui est étendue tant qu’il y a au moins un délégué
qui le référence.
Le compilateur C# permet également l’inférence pour les délégués c'est-à-dire le fait de
pouvoir directement assigner le nom d’une méthode à une référence de type délégué. Nous
retrouvons cette utilisation dans l’exemple ci avant à la ligne 16.
Nous pouvons également reprendre l’exemple de début de paragraphe et l’adapter à le lumière
de ce qui vient d’être dit :

1- La création d’un type délégué.


delegate bool Compare(object x, object y);

2- La création de la fonction.
static bool CompInt(object x, object y)
{
return ((int)x<(int)y);
}

3- L’instanciation du type délégué avec le nom de la méthode.

Compare comp = Compare;

8. Les classes génériques (c# 2.0).

8.1. Introduction.

Les génériques permettent de répondre au problème qui se pose aux langages de


programmation fortement typés quand on veut manipuler des données sans se soucier de leur
type. Avant les génériques, on pouvait utiliser le type object dont tout objet C# hérite. Le
framework 1.1 contient d’ailleurs beaucoup de classes manipulant des objets de type object,
spécialement dans le namespace [Link].
Nous pouvons reprendre notre méthode mise en place permettant d’assurer la comparaison
d’objets pour assurer le tri de notre tableau d’entier ou du tableau de clients du bar en fonction
des dettes.

30 static bool CompInt(object x, object y)


31 {
32 return ((int)x<(int)y);
33 }
34
35 static bool CompClient(object x, object y)
36 {
37 Client tmp1= (Client)x;
38 Client tmp2= (Client)y;
39 return ([Link]<[Link]);
40 }
41

Afin de ne pas modifier l’appel à la méthode, nous utilisons comme paramètre des références
sur la classe object du fait que toute référence d’une classe de base peut référencer n’importe
quel objet d’une classe dérivée. Un gros ennui est sans doute les opérations de transtypage
(pour les types référence) et de boxing (pour les types valeur) que l’on retrouve dans la
méthode de comparaison et qui pour cette dernière sont pénalisants pour la rapidité
d’exécution de votre programme.
Les génériques répondent à ces problèmes de contrôle de type et de performance. En effet, ils
permettent à une classe, méthode ou autre de garder un typage fort tout en traitant un
problème non spécifique à un type particulier.

8.2. Création des types génériques.

Nous reprendrons notre exemple de tri pour les objets de type clients et de type entiers en
créant une classe générique. Nous retrouverons la classe Client ansi qu’une classe Collection
dans laquelle nous retrouverons un tableau d’objet qui pourra être de type client mais qui
pourrait contenir aussi contenir des entiers.
Pour permettre d’effectuer une comparaison aisée, quelle que soit le type d’objet, nous
retouverons dans chaque classe une methode CompareTo dont la déclaration se trouve
renseignée dans l’interface IComparable.

1 class Client:IComparable<Client>
2 {
3 public string Nom;
4 double dettes;
5 public double Dettes
6 {
7 Set
8 {
9 dettes = value;
10 }
11 Get
12 {
13 return dettes;
14 }
15 }
16 public Client(string Nom, double dettes)
17 {
18 [Link] = Nom;
19 [Link] = dettes;
20 }
21 public Client(string Nom): this(Nom, 0.0)
22 {
23 }
24 public static explicit operator double(Client x)
25 {
26 return [Link];
27 }
28 public int CompareTo(Client x)
29 {
30 return ([Link]([Link]));
31 }
32 }

La classe Client hérite de l’interface générique ICompable, ce qui va nous obliger à créer une
méthode CompareTo retournant un entier permettant d’indiquer si l’objet client référencé par
« this » est plus petit, plus grand ou égal à l’objet x passé en paramètre. Nous avons convenu
de comparer les clients en fonction de leurs dettes, ce que nous retrouvons à la ligne 30 en
utilisant la valeur retournée lors de la comparaison des deux membres « Dettes ».

1 class Collection<T>
2 where T:IComparable<T>
3 {
4 public List<T> tab = new List<T>();
5
6 public int Compare(T x,T y)
7 {
8 return [Link](y);
9 }
10
11 public void Add(T x)
12 {
13 [Link](x);
14 }
15 public T this[int i]
16 {
17 Get
18 {
19 return tab[i];
20 }
21 }
22 public void tri()
23 {
24 T temp;
25 for (int i = 0; i < [Link]; i++)
26 {
27 for (int j = i + 1; j < [Link]; j++)
28 {
29 if (Compare(tab[j], tab[i])<0)
30 {
31 temp = tab[i];
32 tab[i] = tab[j];
33 tab[j] = temp;
34 }
35 }
36 }
37 }
38 }

Nous pouvons renseigner dans notre classe générique collection le fait que cette classe
puisse être liée à des types qui seront passés en paramètre. Nous choisissons une lettre ou
un ensemble de lettres séparées par une virgule entre signes < et >.
Dans notre exemple, nous retouvons la classe collection<T> avec, nouveauté pour le c#,
une restriction sur le type de paramètre qui sera dans notre exemple de type
IComparable<T>, ce qui permettra à notre classe de savoir que toute variable de type T
déclarée dans la classe pourra utiliser la méthode CompareTo. La restriction peut être liée
à une seule classe ou plusieurs interfaces.
Dans l’espace de nom [Link], il nous est possible d’utiliser
une liste d’objets, définie sous la forme d’une classe générique Liste<T>. Cette classe
nous permet d’ajouter en dynamique des objets de nature diverses qui pourrait donc être
des entiers, des clients, des flottants… En voici la syntaxe de déclaration que l’on
retrouve à la ligne 4 : public List<T> tab = new List<T>(). Nous retrouvons
plusieurs méthodes utilisant le type T renseigné lors de la déclération de la classe dont en
voici certaines :

-1- La méthode public void Add(T x) qui permet d’ajouter une référence sur un type
client lorsque nous utilisons la syntaxe suivante
CollectionManu<Client> test2 = new Collection<Client>();
[Link](new Client("Dupond1", 14.12));

Ou un type entier dans le cas suivant

Collection<int> test3 = new Collection<int>();


[Link](10);

Nous remarquons la manière d’instancier la classe qui consiste notamment à renseigner le


type utilisé dans la classe, soit dans notre exemple « Client » ou « int ».

-2- L’indexeur public T this[int i] qui retourne grace à l’assesseur un des objets
de la liste tab présent à l’indice i.

8.3. Les énumérateurs.

Pour mieux comprendre la notion d’itérateurs, il est important de reprendre une notion
existant déjà en C#1.1. Nous avons envisagé dans le cadre du cours, une nouvelle
instruction du C#, connue des programmeurs java mais inconnue des programmeurs c ou
c++ : l’instruction foreach. Soit l’exemple suivant :

string []tab ={"Dupond","Dubart","Dupuis"};


foreach (string nom in tab)
{
[Link](nom);
}

Nous allons étudier la façon dont nous pourrions implémenter dans notre classe collection
les méthodes indispensables nous permettant alors de pouvoir utiliser le code suivant :

Collection<Client> test2 = new Collection<Client>();

[Link](new Client("Dupond1", 14.12));


[Link](new Client("Dupond2", 11.12));
[Link](new Client("Dupond3", 14.12));
[Link](new Client("Dupond4", 13.12));

foreach (Client x in test2)


[Link]([Link]);

Nous devons utiliser les interfaces IEnumerable et IEnumerator. Ces deux interfaces nous
permettent d’intégrer dans notre classe l’utilisation des enumérateurs qui sont des objets
permettant de nous déplacer dans un tableau ordonné d’items. Reprenons notre exemple
de la classe Collection et implémentons dans un premier temps une classe de type
IEnumerator.
1 class MonEnumerateur: IEnumerator<T>
2 {
3 int CurIndex;
4 Collection<T> collection;
5 public MonEnumerateur(Collection<T>collection)
6 {
7 [Link]=collection;
8 CurIndex=-1;
9 }
10 public T Current
11 {
12 get {
13 if ([Link] < [Link])
14 return ([Link][[Link]]);
15 else throw new IndexOutOfRangeException();
16 }
17 }
18 public void Dispose() { }
19 object [Link]
20 {
21 get
22 {
23 return Current;
24 }
25 }
26 public void Reset() { CurIndex = -1; }
27 public bool MoveNext()
28 {
29 if (CurIndex < [Link] - 1)
30 {
31 CurIndex++;
32 return true;
33 }
34 else return false;
35 }
36 }
37 }

L’implémentation de l’interface IEnumerator<T> nous oblige à prévoir les membres


suivants dans notre classe :

-1- La propriété public T Current nous permettant de récupérer l’item courant. Les
informations disponibles sur le site msdn de Microsoft nous renseigne l’obligation de
prévoir la version non générique de la propriété Current sous la forme suivante :
object [Link]. L’oubli de cette propriété
provoque une erreur à la compilation.

-2- La méthode public void Reset() nous permettant de nous positionner sur l’item
courant de la position d’origine.

-3- La méthode public bool MoveNext() nous permettant de nous déplacer sur l’item
suivant correspondant à l’odre de ceux-ci dans le tableau.

Attention : la classe MonEnumerateur est une classe imbriquée dans la classe Collection.
Une fois cette classe créée, nous pouvons implémenter l’interface IEnumerable dans
notre classe collection comme dans notre exemple :
1 class Collection<T> :IEnumerable<T>
2 where T:IComparable<T>
3 {
4 public List<T> tab = new List<T>();
5 public IEnumerator<T> GetEnumerator()
6 {
7 return new MonEnumerateur(this);
8 }
9 [Link]
[Link]()
10 {
11 return GetEnumerator();
12 }

L’implémentation de l’interface IEnumerable<T> nous oblige à prévoir la méthode


GetEnumerator dans notre classe. Le rôle de cette méthode est d’instancier notre classe
MonEnumerateur et de simplement retourner cette instance. Les informations
disponibles sur le site msdn de Microsoft nous renseigne l’obligation de prévoir la
version non générique de la méthode GetEnumerator sous la forme suivante :
object [Link]. L’oubli de cette propriété
provoque une erreur à la compilation.
Il est évident que l’implémenation de ces objets n’est pas très simple. C#, sous sa version
2.0, simplifie la mise en place des itérateurs mais la rend aussi plus puissante.
Il n’est en effet plus nécessaire de prévoir une classe héritant de l’interface
IEnumerator. Nous modifierons la méthode GetEnumerator de notre classe Collection
en y utilisant le mot clef yield return de sorte qu’elle retourne elle-même l’ensemble des
différents objet en ayant même la possibilité de les filtrer ou de les trier dans cette
méthode.

1 class Collection<T>
2 where T:IComparable<T>
3 {
4 public List<T> tab = new List<T>();
5 public IEnumerator<T> GetEnumerator()
6 {
7 for (int i=0;i<[Link];i++)
8 {
9 yield return [Link][i];
10 }
11 }
12

Nous pouvons également intégrer dans notre collection plusieurs itérateurs, chacun
parcourant la collection différemment. Par exemple, nous pourrions parcourir nos clients
dans l’ordre inverse sans devoir refaire appel à la méthode de tri.

1 class Collection<T>
2 where T:IComparable<T>
3 {
4 public List<T> tab = new List<T>();
5 public IEnumerator<T> GetEnumerator()
6 {
7 for (int i=0;i<[Link];i++)
8 {
9 yield return [Link][i];
10 }
11 }
12
13 public IEnumerable<T> Reverse
14 {
15 Get
16 {
17 for (int i = [Link]-1; i>=0; i--)
18 {
19 yield return [Link][i];
20 }
21
22 }
23 }

L’itérateur doit être mis en place sous la forme d’une propriété dont le type de retour est
IEnumarable <type>. Dans notre cas, le fait d’avoir une classe générique nous permet
l’utilisation du type <T>. Pour l’utilisation de cet itérateur dans notre fonction principale,
nous retrouverons la synatxe suivante :

1 foreach (Client test in [Link])


2 {
3 [Link]([Link]);
4 }

Il y a quelques limitations sur la façon dont nous pouvons implémenter l’instruction yield
return dans notre code. Une méthode ou une propriété qui a l’instruction yield return ne
peut pas contenir une instruction return simple parce qu’elle provoquerait une
interruption impropre dans l’itération. Nous ne pouvons pas utiliser yield return dans une
méthode anonyme, ni être placée dans une instruction try avec un bloc catch (exclus aussi
dans le bloc catch ou le bloc finally).

9. Le type partial.

La version 1.1 du c# nous oblige à placer l’entierté du code pour une classe donnée dans
un seul fichier. C# 2.0 nous permet de scinder la définition et l’implémentation d’une
classe sur de multiples fichiers. Nous pouvons donc placer le code d’une partie de la
classe dans un fichier et une autre partie de la classe dans un fichier différent en utilisant
juste le mot clef partial. Le support du type partial est envisageable pour les classes, pour
les structures et les interfaces mais il ne peut être utilisé pour les énumérations. Alors que
dans le cas du développement en visual [Link], il est toujours délicat de modifier le
code généré par l’outil de développement au risque de voir son travail personnel perdu si
la classe doit être regénérée, l’utilisation des classes partielles permet d’utiliser la
technique du « code-beside class » stockant la partie de code générée par l’outil dans un
fichier différent.
Cette technique permet aussi à plusieurs développeurs de travailler sur la même classe
sans avoir à vérifier leurs fichiers et ce sans interférence.
Il ne faut pas perdre de vue malgré tout quelques aspects non cumulatifs dans les classes
ou structures :
• La visibilité (public ou internal)
• La classe de base. Une même classe de type partial définie dans plusieurs fichiers
ne peut se voir hériter dans ces déclarations multiples de classe de bases
distinctes.
• Seulement une des classes redéfinie peut implémenter une interface.
• Seulement une des classes redéfinie peut surcharger une méthode abstraite ou une
méthode virtuelle.

10. Les classes statiques.

Il est fréquent en C# de retrouver des classes statiques qui ne comprennent que des
membres statiques ou des méthodes statiques. Dans ce cas, une instanciation de ces
classes n’a pas de raison d’être. Pour que nous ne puisssions pas instancier de telles
classes, la seule solution en C#1.1 est de rendre le constructeur par défaut privé. Sans
constructeurs publiques, il n’est pas possible d’instancier une classe de ce type.
C# 2.0 supporte maintenant les classes statiques en permettant l’ajout du mot clef statique
dans la définition de la classe public static class MaClasse{ }. Une telle classe ne peut
avoir de méthodes instanciables et ne peut servir comme classe de base dans un héritage.

11. Le qualifieur d’espace de nom global ::

Il est possible d’utiliser un espace de nom imbriqué qui a le même nom qu’un des espaces
de noms globaux. Dans notre exemple, nous retrouvons imbriqué notre espace de nom
System. Dans un tel cas, le compilateur a des difficultés à résoudre la référence à cet
espace de nom global et ne parviendra pas à compiler la ligne 10.

1 namespace ConsoleApplication2
2 {
3
4 namespace System
5 {
6 class Program
7 {
8 static void Main(string[] args)
9 {
10 [Link]("bonjour");
11 }
12 }
13 }
14 }

Le C# 2.0 permet l’utilisation du qualifieur d’espace de nom global :: pour indiquer au


compilateur qu’il doit démarrer sa recherche dans le scope global. Nous pouvons
appliquer le qualifieur :: à la fois aux espaces de noms et aux types. Le qualifieur sera
précédé du mot clef global ou de tout alias. Dans notre exemple, la ligne 10 pourra donc
être remplacée par la syntaxe suivante :

global::[Link]("bonjour");

1 using test=System ;
2 namespace ConsoleApplication2
3 {
4 namespace System
5 {
6 class Program
7 {
8 static void Main(string[] args)
9 {
10 test::[Link]("bonjour");
11 }
12 }
13 }
14 }
Voici un autre exemple mettant en évidence l’utilisation du qualifieur pour les types :

1 namespace MyApp
2 {
3 class MyClass
4 {
5 static void Main()
6 {
7 }
8 public void MyMethod()
9 {
10 global::MyClass obj = new global::MyClass();
11 [Link]();
12 }
13 }
14 }
15 public class MyClass
16 {
17 public void MyMethod()
18 {
19 global::[Link]("Hello");
20 }
21 }

12. Nullable types.

Le problème rencontré avec la version C#1.1 pour les types valeur provenait des
difficultés lors de liaisons avec de sbases de données pour ces variables puissent contenir
la valeur null qui est en fait réservée pour les types références ne référençant aucun objet.
Ce nouveau type est construit en utilisant le ‘?’. Si nous désirons créer une variable de
type entière capable de contenir la valeur null, nous aurons alors la syntaxe suivante :

int? x = null ;

Un tel type est un fait une instance de la structure [Link] et va, en plus de fournir
la fonctionnalité habituelle du type int dans notre exemple, permettre à la variable de
contenir également la valeur null. La structure offre également les deux proriétés :
• HasValue qui retournera true si la variable contient une valeur et false si le
contenu est null
• Value qui retournera la valeur assignée à la variable et dans le cas contraire, une
exception de type [Link] sera levée.

1 namespace ConsoleApplication4
2 {
3 class NullableExample
4 {
5 static void Main()
6 {
7 int? num = null;
8 if ([Link] == true)
9 {
10 [Link]("num = " + [Link]);
11 }
12 Else
13 {
14 [Link]("num = Null");
15 }
16
17 int y = [Link]();
18
19 Try
20 {
21 y = [Link];
22 }
23 catch ([Link] e)
24 {
25 [Link]([Link]);
26 }
27 }
28 }
29 }

Nous pouvons nous poser la question de savoir ce qui se passe lorsque nous voulons
utiliser des opérateurs arithmétiques avec ces types ou égélement vouloir utiliser des
opérateurs logiques lorsque les variables sont de type bool? .
Les opérateurs prédéfinis, unaires et binaires, ainsi que les surcharges d’opérateurs qui
existent pour des opérandes de type valeur peuvent être aussi utilisés pour des nullable
type. Ces opérateurs produisent une valeur null si les opérandes ont comme valeur null;
autrement, l’opérateur utilise la valeur contenue pour calculer le résultat. Lorsque des
comparaisons sont effectuées sur ces types et que l’une ou l’autre des valeurs est nulle, le
résultat de la comparaison sera toujours false.
Une variable de type bool? Peut contenir trois valeurs différentes : true, false et null. De
ce fait, elles ne peuvent être utilisées dans des instructions conditionnelles de type if, for
ou while. Nous reprenons une table de vérité pour les opérateurs logiques comprenant des
opérandes de ce type :

x y x&y x|y
true true True True
true false False True
true null Null True
false true False True
false false False False
false null False Null
null true True True

Nous retrouvons également pour nouveaux types, le nouvel opérateur binaire ??. Cet
opérateur a comme opérande de gauche une variable de type ‘nullable’ et renverra sa
valeur si elle n’est pas nulle tandis qu’il renverra le contenu de l’opérande de droite dans
le cas contraire. Prenons l’exemple suivant :
1 static void Main(string[] args)
2 {
3 int? x = null;
4 int b = 20;
5 int y = x ?? b;
6 [Link]("Contenu de y:" + [Link]());
7 }

Dans cet exemple, l’exécution du code correspondant provoquera comme affichage :


contenu de y: 20

13. Inférence des types locaux.

Sous cette appellation, se cache l'utilisation du mot clef var. Ce mot clef n'est pas étranger
pour certains puisqu'on le retrouve dans d'autres langages tel que par exemple le
javascript. Il faut malgré tout y placer une nuance importante: l'aspect dynamique du type
de la variable qui est géré lors de l'exécution du code.
En c#, il n'en est rien. Le mot clef var remplace en fait tout type que vous utiliseriez et
c'est le compilateur qui se chargera d'identifier le type de la variable, d'où la nécessité
d'initialiser la variable lors de la déclaration.
L'utilisation du mot clef var devra être vue en c# comme une simplification de la syntaxe
d'écriture de certaines déclarations comme nous le verrons plus loin.

1 var entier=10;
2 [Link]([Link]().Name);
3 [Link]();

Le code suivant provoquerait une erreur lors de la compilation.

1 var entier;
2 [Link]([Link]().Name);
3 [Link]();

L'erreur obtenue est la suivante: Les variables locales implicitement typées doivent être
initialisées.

Il n'est pas possible non plus d'affecter la valeur nulle ni de changer le type de contenu
d'une variable dans la suite du code. Le mot clef var ne se limite pas à la déclaration de
variable simple mais aussi de tout objet.
Imaginons la classe des clients du cercle des étudiants.

1 class Clients
2 {
3 private string m_Nom;
4 private float m_Dettes;
5 public Clients(string Nom, float Dettes)
6 {
7 this.m_Nom = Nom;
8 this.m_Dettes = Dettes;
9 }
10 }
11 class Program
12 {
13 static void Main(string[] args)
14 {
15 var Client1 = new Clients("Dupond", 12.5F);
16 [Link]();
17 }
18 }

Il est également possible de déclarer des tableaux. La syntaxe sera alors la suivante:

1 var tableau = new[] { 1, 2, 3, 4, 5 };


2 [Link]([Link]().Name);
3 [Link]();

Aucun type ne doit être utilisé dans la déclaration du tableau, même pas après l'opérateur
new.

Nous terminerons l'utilisation du mot clef var par la mise en évience de la simplification
de la syntaxe. Imaginons que l'on veuille associer à un bar une collection de clients avec
l'historique de leurs consommations au bar. Nous allons dans notre cas travailler avec une
collection de type Dictionary comme dans l'exemple suivant:

Dictionary<Clients, List<float>> bar = new


Dictionary<Clients,List<float>>()
[Link](new Clients("dupond", 12.5F), new List<float>())

En adoptant le mot clef var, nous obtiendrons alors:

var bar = new Dictionary<Clients, List<float>>();

14. Les méthodes d'extension.

En C#1.0, l'ensemble des déclarations/définitions des méthodes se trouvaient dans la


même classe.
En C#2.0, grâce à l'emploi du mot clef partial, il ets possible de placer des méthodes dans
une classe ou une autre pour autant qu'elles portent le même nom.
En C#3.0 nous pouvons ajouter à une classe des méthodes d'extension. Ces méthodes ne
sont pas définies dans la classe elle-même mais définies dans une autre classe statique.
Ce concepte vient à l'encontre de ce qui a été dit précédemment:

Une classe statique n'est pas instanciée. Comment une méthode d'une telle classe
pourrait-elle alors interagir avec les membres d'une classe instanciée?
Pour les méthodes d'une classe instanciée, nous utilisons l'opérateur this. En fait,
lorsqu'une méthode instanciée est appelée par l'objet, celui-ci est passé en argument de
façon implicite et récupéré par l'opérateur this.
Il en sera de même pour les méthodes de la classe statique lorsqu'elles seront appelées.

Imaginons que l'on souhaite créer la gestion d'une pile de type "premier entré premier
sorti". Nous possédons des listes génériques en .net mais il manque principalement une
méthode dépilement. En c#2.0 nous aurions créé une nouvelle classe dans laquelle nous
aurions ajouté les méthodes souhaitées d'empilement et de dépilement.
En c#3.0, voici comment nous pouvons ajouter de nouvelles méthodes à une classe
existante. Il faut remarquer l'utilisation de méthodes d'extension comme étant génériques
et de ce fait, elles peuvent être utilisées quelle que soit la nature de la pile avec laquelle
on travail: pile d'entiers, de flottant, de chaînes de caractères...

1 static class PileExtend


2 {
3 public static void Empile<T>(this List<T> Pile, T valeur)
4 {
5 [Link](valeur);
6 }
7
8 public static T Depile<T>(this List<T> Pile)
9 {
10 if ([Link] == 0) throw new Exception("pile vide");
11 T valeur;
12 valeur = Pile[[Link] - 1];
13 [Link]([Link] - 1);
14 return valeur;
15 }
16
17 }
18 class Program
19 {
20 static void Main(string[] args)
21 {
22 var Pile = new List<int>();
23 [Link](10);
24 var Resultat = [Link]();
25 [Link]("valeur dépilée:{0}", Resultat);
26 [Link]();
27 }
28 }

D'un point de vue syntaxique, nous remarquerons l'emploi de l'opérateur this dans la
déclaration du premier argument de chacune des extensions de méthode

Quelques remarques:

- Lorsque la classe possède une méthode de classe possédant le même nom et les mêmes
nombre et types d'arguments qu'une des méthodes d'extension, c'est la méthode de classe
qui sera appelée.
- La remarque précédente vaut aussi pour les méthodes dont on hérite d'une classe de
base.
- L'emplacement de la méthode appelée n'est pas déterminée de façon dynamique, ce qui
ne pénalise pas les performance de votre application.
- Une classe pour laquelle on retrouverait des méthodes disséminées dans d'autre classe
statiques rendrait vite votre code difficile à gérer. Donc le principe de l'héritage prévaut
sur le choix des extensions de méthodes. Dans certains cas particuliers, il ne sera pas
possible d'y échapper:
• Si la classe est déclarée avec le mot clef sealed (dont on ne peut hériter)
• Nous désirons implémenter une méthode qui pourrait être invoquée dans
plusieurs classes héritant d'une interface commune.
• Certains objets sont déjà instanciés et il ne vous est pas possible ni de modifier
la classe dans votre code ni de changer les instantiation vers une classe dérivée.

15. Les expression lambda.

Dans la version 2.0 du C#, nous avons vu qu'il existait des méthodes anonymes,
principalement utiles lors de la création de délégués. Les expressions lambda du c# 3.0
vont encore simplifier la création de ces méthodes anonymes.
La création de cette expression se réalise au moyen de l'opérateur =>. Comme opérande
de gauche, on retrouvera le ou les arguments tandis que pour l'opérande de droite, on
retrouvera la partie de code qui notamment retournera le résultat.

Nous retrouverons les formes suivantes dans les expressions lambda:

x=>x+10
x=>{return x*x;}
(int x)=>x*10
( x,y)=>{x++; return x/y;}
(ref int x,int y){x++; return x/y;}
( )=> new Beer( )

Reprenons l'évolution de code entre le C#1.0 et 2.0 pour l'utilisation des délégués:

• En C# 1.0 sans les méthodes anonymes:

delegate bool Compare(object x, object y);


static bool CompInt(object x, object y)
{
return ((int)x<(int)y);
}
Compare comp = new Compare(CompInt);

tri(clientBar,comp);

• En C#2.0 avec les méthodes anonymes:


delegate bool Compare(object x, object y);
Compare comp = delegate (object x, object y)
{
return ((int)x<(int)y) ;
};

tri(clientbar,comp);

• En C#3.0 avec les expressions lambda:

tri(clientbar,(x,y)=>return ((int)x<(int)y);)

Reprenons un autre exemple plus complet que les simples lignes de code reprises ci
avant. Nous souhaitons pouvoir trier les noms des clients repris dans une liste générique.

1 List<string> Clients=new List<string>{"Dupond","Dubard","Bertrand" };


2 [Link](delegate(string x, string y)
3 {
4 return ([Link](y));
5 });
6 foreach (string client in Clients)
7 {
8 [Link](client);
9 }
On notera que le Framework définit l’ensemble suivant des délégués utilisables de la même
façon que Compare dans notre exemple:

• public delegate T Func< T >();


• public delegate T Func< A0, T >( A0 arg0 );
• public delegate T Func<A0, A1, T> ( A0 arg0, A1 arg1 );
• public delegate T Func<A0, A1, A2, T >( A0 arg0, A1 arg1, A2 arg2 );
• public delegate T Func<A0, A1, A2, A3, T> ( A0 arg0, A1 arg1, A2
arg2, A3 arg3 );

Nous retrouverons également l'emploi du mot clef Action ne retournant aucun type
contrairement au mot clef Func

16. Les expressions d'initialisation des objets et collections.

a) Initialisation des objets.

Il existe plusieurs façon d'initialiser les membres d'une classe lors de son instanciation.
Soit nous utilisons les arguments passés par le constructeur, soit nous utilisons les
propriétés accessibles en écriture c-à-d implémentant l'assesseur set.
Les expressions d'initialisation sont adaptées à l'utilisation des propriétés. Reprenons
notre exemple de clients d'un bar et adaptons le pour faire ressortir des propriétés.

1 static void Main(string[] args)


2 {
3 Clients x = new Clients();
4 [Link] = "Dupond";
5 [Link] = 12.5F;
6
7 Clients y = new Clients()
8 {
9 Nom = "Dupont",
10 Dettes = 15.5F
11 };
12
12 [Link]([Link]+" "+[Link]());
14 [Link]();
15 }
16
17 class Clients
18 {
19 private string m_Nom;
20 private float m_Dettes;
21 public string Nom
22 {
23 Set
24 {
25 this.m_Nom = value;
26 }
27 Get
28 {
29 return this.m_Nom;
30 }
31 }
32 public float Dettes
33 {
34 Set
35 {
36 this.m_Dettes = value;
37 }
38 Get
39 {
40 return this.m_Dettes;
41 }
42 }
43
44 }

Si nous envisageons l'utilisation d'une liste de clients qui dans notre exemple pourrait être
un bar, nous pourrons alors envisager la syntaxe suivante:

1 static void Main(string[] args)


2 {
3 List<Clients> Bar = new List<Clients>();
4 [Link]( new Clients{
5 Nom="Dupond",
6 Dettes=15.5F
7 }
8 );
9 }

Nous pouvons aller encore plus loin dans la simplification de la syntaxe en incluant
l'utilisation des types anonymes par l'utilisation du mot clef var

b) Les types anonymes.

1 static void Main(string[] args)


2 {
3 var Client1 = new {
4 Nom="Albert",
5 Dettes=12.5F
6 };
7
8 [Link]([Link]);
9 [Link]();
10 }

Nous pouvons remarquer que la classe ne possède aucun nom, que la variable Client1
ne possède aucun type excepté le mot clef var qui est utilisé. Les différents membres de
cette classe anonyme ne possèdent aucun type et comme renseigné lors du parapgraphe
précédent traitant des inférences, ceux-ci seront définis lors de la compilation suivant
les types des données utilisées lors de leur initialisation.

c) L'équivalence des types anonymes.


1 var Client1 = new {
2 Nom="Albert",
3 Dettes=12.5F
4 };
5
6 var Client2 = new
7 {
8 Dettes = 15.6F,
9 Nom = "Dupont"
10 };
11
12 Client1 = Client2;

La compilation de cette partie de code va provoquer l'erreur comme quoi il n'est pas
possible de convertir implicitement les types anonymes entre eux. Si nous permuttons les
membres Dettes et Nom dans la deuxième déclaration, l'erreur disparaît. Il est donc
important de respecter l'ordre des membres. Si vous supprimer l'un des F qui suit la
constante flottante, nous aurons la même erreur.
Donc, le nombre de champs, l'odre des champs et leur type doivent être respectés.

d) Les projections.

1 string Nom="Dupond";
2 float Dettes=12.5F;
3 var Client1 = new {
4 Nom,
5 Dettes
6 };

Lorsque une variable a déjà été déclarée et initialisée précédemment, celle-ci peut être
utilisée lors de la déclaration du type anonyme. Dans notre exemple, les variables Nom et
Dettes sont typées mais nous aurions également pû utiliser le mot clef var.
Imaginons que chaque nouveau client soit tiré d'une liste existante. Nous pourrions alors
envisager la syntaxe suivante:

1 List<etudiants> Classe = new List<etudiants>();


2 [Link](new etudiants
{ Nom = "Dupond", Matricule = "121" });
3 [Link](new etudiants
{ Nom = "dubard", Matricule = "545" });

4 var Client1 = new {


5 Classe[0].Nom,
6 Classe[1].Matricule
7 };

17. Introduction aux requêtes LINQ (Language Intregrated Query).

Celles ou ceux familiés avec le langage SQL ne devraient pas rencontrer de difficultés
avec ce langage de requête. Si le langage SQL se limite à interroger des bases de
données, Linq permet d'intérroger une multitude de structures de données. Nous
retrouverons en effet DLinq permettant de traduire le langage linq en SQL pour intéroger
les bases de données. Nous retrouvons également XLinq permettant d'interroger des
documents XML,Olinq pour linq to Object, Elinq pour linq to Entity (entity framework),
Slinq pour linq to SharePoint.
L’object de cette nouvelle syntaxe est d’enviager une couche rendant indépendant le
langage d’accès à ces données indépendemment de leur structure. Dans les requêtes linq,
nous retrouvons les clauses suivantes :
- La clause du choix de la source de données : from
- La clause de filtrage des données : where
- La clause de tri : orderby
- La clause de regroupement : group …. by
- La clause de jointure : join … on
- La clause de sélection : select
Pour comprendre l’utilisation de cette syntaxe, nous allons reprendre quelques exemples
basés sur l’interrogation d’une liste générique. Nous prendrons une classe associée à un
étudiant et la liste générique à une école.

class Etudiant
{
public string Nom { get; set; }
public string Prenom { get; set; }
public int Bloc { get; set; }
}

List<Etudiant> Ecole = new List<Etudiant>();

[Link](new Etudiant { Nom="dupond", Prenom="bernard", Bloc=1 });


[Link](new Etudiant { Nom="durand", Prenom="jean", Bloc=1 });
[Link](new Etudiant { Nom="dubart", Prenom="eric", Bloc=1 });
[Link](new Etudiant { Nom="lambert", Prenom="christophe", Bloc=2 });
[Link](new Etudiant { Nom="nemard", Prenom="jean", Bloc=2 });
[Link](new Etudiant { Nom="ris", Prenom="jean", Bloc=3 });

var classe1 = from etudiant in Ecole where [Link] == 1 orderby [Link]


select etudiant;
var Liste1 = [Link]<Etudiant>();

var classe2 = [Link](x => [Link] == 1).OrderBy(x => [Link]).Select(x => x);

18. Les nouveautés dans le Csharp 4.0.

18.1. Les paramètres optionnels.

Pour les habitués du langage C++, cette possibilité manquait bien au csharp. Voici enfin
une lacune comblée dans la version 4.0 du langage. Elle permet dans la déclaration d'une
méthode de définir des paramètres optionnels c-à-d dont la valeur par défaut est définie si
lors de l'appel, ce paramètre n'était pas passé.
Pour rappel, ce manquemant nous obligeait à surcharger les constructeurs et prévoir un
chaînage pour autoriser de multiples instanciation avec des nombres d'arguments fournis
différents.
Prenons le cas d'une classe représentant un client. Lorsqu'un nouveau client est créé, par
défaut son contrat porte sur une période de 1an sauf si une date est fournie lors de l'appel
du constructeur.
1 class client
2 {
3 private string Nom;
4 private string Prenom;
5 DateTime ContratLimite;
6
7 public client(string Nom, string Prenom, int duree=12)
8 {
9 [Link] = Nom;
10 [Link] = Prenom;
11 [Link] = [Link](duree);
12 }
13
14 public override string ToString()
15 {
16 return [Link] + " " + [Link] +" "+
[Link]();
17 }
18 }

1 static void Main(string[] args)


2 {
3 client cl1 = new client("Dubart", "Eric");
4 client cl2 = new client("Dupond", "Jean", 24);
5 [Link]([Link]());
6 [Link]([Link]());
7 [Link]();
8 }

Les remarques suivantes sont importantes à signaler:

Tous les paramètres facultatifs doivent apparaître après les paramètres requis. La syntaxe
suivante ne serait donc pas autorisée:

public client(int duree=12, string Nom, string Prenom)...

Lors de l'appel de la méthode, la remarque suivante est également importante:

Lors d'un appel d'une méthode, tout paramètre optionnel initialisé nécessite que tous les
paramètres optionnels précédents le soit. Modifions notre code de la façon suivante:

1 enum categorie
2 {
3 professionnel,
4 Particuler
5 };
6
7 class client
8 {
9 private string Nom;
10 private string Prenom;
11 DateTime ContratLimite;
12
13 public client(string Nom, string Prenom, int duree=12, categorie
type=[Link])
14 {
15 [Link] = Nom;
16 [Link] = Prenom;
17 [Link] = [Link](duree);
18 }
19
20 public override string ToString()
21 {
22 return [Link] + " " + [Link] +" "+
[Link]();
23 }
24 }

L'utilisation du constructeur sous la forme suivante provoquera une erreur à la compilation:

client cl1 = new client("Dubart", "Eric",,[Link]);

Il faudra donc utiliser la syntaxe suivante:

client cl1 = new client("Dubart", "Eric",12,[Link]);

Ou alors se réferer au paragraphe suivant qui offre une autre solution en proposant les
paramètres nommés.

18.2. Les paramètres nommés.

Une méthode peut être appelée en renseignant le nom du paramètre suivi de l'opérateur : et
ensuite la valeur que l'on souhaite affecter à ce paramètre.
de ce fait, nous pouvons appeler une méthode (constructeur y compris) en mettant les
paramètres dans l'ordre dans lequel on souhaite les placer.

En prenant l'exemple du paragraphe précédent, nous pouvons instancier la classe client en


appelant le constructeur de la façon suivante:

client cl3 = new client(Prenom:"Jacques",Nom: "Durand",type:[Link]);

Nous pouvons remarquer que les paramètres <Prenom> et <Nom> ne sont pas passés dans le
même ordre que celui renseigné dans la déclaration du constructeur. Nous pouvons également
renseigner le paramètre <type> sans devoir renseigner le précédent.

18.3. Le typage dynamique.


Le typage dynamique (DLR) fait partie intégrante du CLR. Comme nous avons pu le
renseigner dans les différentes versions du langage Csharp, ce dernier est un langage très typé.
Pour faciliter l'écriture du code, sous l'intitulé de l'inférence des types locaux, Csharp a
introduit dans sa version 2 l'utilisation du mot clef var. Contrairement à d'autres langages, ce
mot clef lors d'une déclaration n'enlevait en rien le typage des variables puisque le type était
associé à la donnée affectée à la variable lors de l'initialisation obligatoire lors de la
déclaration.

Mircosoft fait un nouveau pas en incluant maintenant le vrai type dynamique grace au pseudo
type dynamic. Il existait déjà une solution sous la forme de [Link] mais avec dynamic,
aucun transtypage ne doit être réalisé pour appeler une méthode. Les liens seront réalisés de
façon dynamique lors de l'exécution. Etant donné que Visual Studio ne connait pas le type
avant l'exécution, les différentes méthodes ne seront pas proposées par l'intellisense.

1. dynamic MonObject;
2. MonObject = "bonjour";
3. [Link]([Link]().ToString());
4. [Link]([Link]());
5. MonObject = 10;
6. [Link]([Link]().ToString());
7. [Link]([Link]());

Attention: l'utilisation d'une telle possibilité peut amener des erreurs uniquement lors de
l'exécution et non à la compilation.
L'intégration de ce type dynamic permet d'envisager l'utilisation de langages dynamiques tels
que python.

18.4. La co et contra variance pour les délégués.

La covariance est en soit un concept qui existait déjà à l'origine du Csharp. On peut juste
rappeler que toute référence d'une classe de base peut pointer vers un objet d'une classe
dérivée. Si nous prenons l'exemple suivant dépouillé, d'une classe de base <employé> ainsi
que la classe dévivée <commercial>, nous aurons le code suivant:

1 abstract class employe


2 {
3 public string nom { get; set; }
4 public string matricule { get; set; }
5 public int indiceSalaire {get; set;}
6 public employe(string nom, string matricule)
7 {
8 [Link] = nom;
9 [Link] = matricule;
10 }
11 }
12
13 class commercial : employe
14 {
15 public double VenteParMois;
16 public commercial(string nom, string matricule)
17 : base(nom, matricule)
18 {
19 [Link] = 1;
20 [Link] = 0;
21 }
22 }

Nous pouvons retrouver dans la fonction principale le code suivant:

employe emp1 = new commercial("Dupond", "12515");


[Link]([Link]);
[Link]();

Depuis la version 1 du csharp, la même possibilité de covariance existe pour les tableaux. Un
exemple simple est de se baser sur Obect qui est la classe de base de tout objet créé.

Object [] obj = new string[20];


obj[0] = "bonjour";
obj[1] = 10;

Un problème lié à cette façon de faire est la troisième ligne où nous tentons de placer un
entier alors que le tableau doit contenir des chaînes de caractères. Le compilateur n'indiquera
aucune erreur tandis que nous aurons un tel message lors de l'exécution du code.

Nous retrouvons la co et contra variance dans la version 2 du csharp au niveau des délégués.
Nous retrouvons l'exemple suivant tiré du site [Link]

static object GetObject() { return null; }


static void SetObject(object obj) { }

static string GetString() { return ""; }


static void SetString(string str) { }

static void Main()


{
// Covariance. La fonction Getstring retourne une type string tandis que le
délégué renseigne un type de retour object
Func<object> del = GetString;

// Contravariance. L'argument renseigné dans le délégué est un type string tandis que
la méthode SetObject renseigne un type object.

Action<string> del2 = SetObject;

Func<string> del3 = GetString;


Func<object> del4 = del3; // Uniquement accepté à partir du fremwork 4.
}

18.5. La co et contra variance pour les paramètres de type générique.

Reprenons l'exemple des classes <employé> et <commercial> de l'exemple précédent. Nous


pourrions envisager qu'une entreprise ait une liste d'employés ou de commerciaux. Pour ce
faire, nous créons une fonction statique recevant comme paramètre un objet de type interface
générique avec un type <employé> et nous souhaitons lors de l'appel, lui faire passer un
object qui serait une liste générique avec le type <commercial>. Nous savons qu'en théorie, ce
devrait être possible du fait que <commercial> est une classe dérivée de la classe de base
<employé>.

1 static void AfficherEmployés(IEnumerable<employe> Societe)


2 {
3 foreach (employe tmp in Societe)
4 {
5 [Link]([Link]);
6 }
7 }

Avec comme appel de fonction:

List<commercial> entreprise = new List<commercial>();


AfficherEmployés(entreprise);

En utilisant le framework 3.5, la compilation d'un tel code ammène une erreur.

Si nous passons pour le même code à l'utilisation du framework 4.0, l'erreur à la compilation
disparaît. En visualisant la déclaration de l'interface dans le deux framework, nous retrouvons
une différence:

En framework 3.5: [Link]<T>


En framework 4.0: [Link]<out T>

L’utilisation du mot clé out avant le type générique T permet de signifier que le type T pourra
uniquement être utilisé comme type de retour des méthodes définies dans ces interfaces en
autorisant l'utilisation d'un type <dérivé> lors de l'appel en lieu et place du type de <base>
renseigné dans la déclaration. On dit alors que cette interface est « covariante » du type T.

La contravariance sera semblable dans le principe, excepté qu'elle va faire référence aux types
utilisés dans le passage des arguments et non dans les valeurs retournées. On retrouvera
l'usage du mot clef in.

Ce que l'on doit retenir au niveau du framework 4 est qu'un ensemble d'interfaces et de
délégués ont été mis à niveau de sorte d'intégrer ces capacités de co et contravariance.

Les interfaces suivantes ont été adaptées:

•IEnumerable<T> (T est covariant)


•IEnumerator<T> (T est covariant)
•IQueryable<T> (T est covariant)
•IGrouping<TKey, TElement> (TKey and TElement sont covariant)
•IComparer<T> (T est contravariant)
•IEqualityComparer<T> (T est contravariant)
•IComparable<T> (T est contravariant)

Si nous analysons un code associé à l'interface IEnumerable du framework 4, nous aurons:

public interface IEnumerable<out T> : IEnumerable {


IEnumerator<T> GetEnumerator(); }

Le type T de cette interface est bien utilisé comme type de retour pour le GetEnumerator().
Si nous prenons l'interface IComparer, voici un code d'exemple:

public interface IComparer<in T> {


int Compare(T left, T right); }

Le type T est bien utilisé dans la déclaration des arguments de la méthode Compare.

Pour les délégués, nous aurons:

• Action<T>, Action<T1, T2> (T, T1, T2, sont contravariant)


• Func<TResult> and Func<T, TResult> (TResult est covariant; T, T1, T2 contravariant)
•Predicate<T> (T est contravariant)
•Comparison<T> (T est contravariant)
•Converter<TInput, TOutput> (TInput est contravariant; TOutput est covariant.)

19. Les nouveautés dans le Csharp 5.0.

Pour rappel dans l'évolution des différentes versions du langage c#

Les deux grandes nouveautés de la version 5 du csharp sont les méthodes asynchrones et les
attributs caller information (information sur l'appelant).

La programmation asynchrone et multi-thread n'est pas nouvelle en csharp. Beaucoup de


programmeur continue le développement mono thread alors que les environnements matériels
actuels multi coeurs et multi thread permettent un gain en performance énorme dans certaines
applications. Ce qui pouvait freiner, c'est probablement l'aspect rebarbatif des techniques
proposées. Nous allons dans le cadre de ce chapitre, aborder globalement cette
programmation.

19.1. Utilisation des threads

Pour rappel, un thread est la plus petite des entités de code qui reçoit des ressources
processeur qui lui sont propres. La programmation multi threading permettra donc de tirer
profit des architectures multi coeur des processeurs actuels.

L'espace de nom que nous utiliserons pour la création d'un thread est [Link] et il
faudra maîtriser les gestion des délégués pour le démarrage d'un nouveau thread. En c#, un
thread sera associé à une méthode. Un thread pourra être démarré, sa sortie naturelle sera celle
liée à la sortie de la méthode. Un thread pourra être mis en pause, repris ou tout simplement
interrompu de façon forcée.

1. namespace WorkerThread
2. {
3. class Program
4. {
5. [STAThread]
6. static void Main(string[] args)
7. {
8. [Link] = new AutoResetEvent(false);
9. Thread t = new Thread(new ThreadStart([Link]));
10. [Link]();
11. [Link]("En attente de la fin du traitement");
12. [Link]();
13. [Link]("Traitement terminé compteur:{0}",[Link]);
14. [Link]();
15. }
16. }
17.
18. class class1
19. {
20. public static int Compteur;
21. public static AutoResetEvent autoEvent;
22. public static void traitement()
23. {
24. Compteur = 0;
25. while (Compteur<500)
26. {
27. Compteur++;
28. [Link](100);
29. }
30. [Link]();
31. }
32. }
33. }
Thread t = new Thread(new ThreadStart([Link]));

ThreadStart est un délégué qui sera instancié avec comme argument la méthode statique qu'il
doit référencer. C'est la méthode qui sera exécutée lorsque le thread sera démarré en utilisant
la méthode start.

while (Compteur<500) {...}


Le thread arrêtera lors de la sortie naturelle de la méthode, à savoir lorsque le compteur
atteindra la valeur 500. L'utilisation de la classe AutoResetEvent permettra de mettre la
fonction main en attente si elle doit attendre le résultat du traitement pour poursuivre son
exécution. (Comme <main> s'exécute sur un autre thread, <main> pourra également
poursuivre son exécution et ses propres traitement au lieu d'attendre. Il existe deux types
d'événements de synchronisation : AutoResetEvent, et ManualResetEvent. Ils diffèrent
uniquement dans la mesure où AutoResetEvent change d'un état signalé à non signalé
automatiquement à chaque fois qu'il active un thread. À l'inverse, un ManualResetEvent
permet l'activation d'un certain nombre de threads par leur état signalé et revient à un état non
signalé uniquement lorsque sa méthode Reset est appelée.

Si l'on souhaite arrêter la fonction main avant que le thread de traitement ne soit terminé, il
faudra veiller à forcer son arrêt. De façon naturelle ou de façon forcée. Voici les deux
solutions envisageables.

1. namespace WorkerThread
2. {
3. class Program
4. {
5. [STAThread]
6. static void Main(string[] args)
7. {
8. [Link] = new AutoResetEvent(false);
9. Thread t = new Thread(new ThreadStart([Link]));
10. [Link]();
11. [Link]("En attente de la fin du traitement");
12. [Link](5000); // debloquage après 5000msec
13. [Link] = true;
14. [Link]("Traitement compteur:{0}",[Link]);
15. [Link]();
16. }
17. }
18.
19. class class1
20. {
21. public static int Compteur;
22. public static AutoResetEvent autoEvent;
23. public static bool Sortie;
24. public static void traitement()
25. {
26. Compteur = 0;
27. Sortie = false;
28. while (Compteur<500 && Sortie==false)
29. {
30. Compteur++;
31. [Link](100);
32. }
33. [Link]("thread terminé");
34. [Link]();
35. }
36. }
37. }
Nous jouerons sur la valeur de la variable statique <Sortie> pour quitter le thread
prématurément avant que le compteur n'atteigne la valeur 500.
Nous avons utilisé la méthode Start( ) pour démarrer le [Link] existe pour la classe thread,
une méthode abort( ) permettant de mettre fin au thread de façon plus brutale. Comme il est
souvent nécessaire de quitter un thread proprement, la méthode Abort( ) sera à la base d'une
exception au niveau de notre méthode statique, qu'il sera possible de gérer au moyen d'un try
catch. Voici le code suivant:

1. namespace WorkerThread
2. {
3. class Program
4. {
5. [STAThread]
6. static void Main(string[] args)
7. {
8. [Link] = new AutoResetEvent(false);
9. Thread t = new Thread(new ThreadStart([Link]));
10. [Link]();
11. [Link]("En attente de la fin du traitement");
12. [Link](5000);
13. [Link]();
14. [Link]("Traitement terminé compteur:{0}",[Link]);
15. [Link]();
16. }
17. }
18.
19. class class1
20. {
21. public static int Compteur;
22. public static AutoResetEvent autoEvent;
23. public static bool Sortie;
24. public static void traitement()
25. {
26. Try
27. {
28. Compteur = 0;
29. Sortie = false;
30. while (Compteur < 500)
31. {
32. Compteur++;
33. [Link](100);
34. }
35. [Link]("thread terminé");
36. [Link]();
37. }
38. catch (ThreadAbortException ex)
39. {
40. [Link]("Sortie forcée du thread");
41. }
42. }
43. }
44. }

19.2. Accès aux ressources communes partagées.

L'exécution simultannée de plusieurs méthodes pose le problème de l'accès à des ressources


communes. Nous devons mettre en place des techniques de verrouillage bloquant l'accès à des
ressources si elles sont utilisées par un autre thread mais libérant ce verrouillage lorsque cet
autre thread a terminé son travail. Cette gestion peut s'effectuer au moyen du mot clef <lock>.
L'argument fourni au mot clé lock doit être un objet basé sur un type référence ; il est utilisé
pour définir la portée du verrouillage.

En général, évitez de verrouiller un type public, ou des instances échappant au contrôle de


votre code. Les constructions courantes lock (this), lock (typeof (MyType)) et lock
("myLock") violent cette directive :
• lock (this) pose problème s'il est possible d'accéder publiquement à l'instance.
• lock (typeof (MyType)) pose problème s'il est possible d'accéder publiquement à
MyType.
• lock(“myLock”) pose problème puisque tout autre code du processus utilisant la même
chaîne partagera le même verrouillage.
La méthode conseillée consiste à définir un objet private à verrouiller, ou une variable objet
private static pour protéger des données communes à toutes les instances.

Nous allons prendre l'exemple d'un compte bancaire pouvant être débité en s'assurant
obligatoirement que le solde ne soit jamais négatif. Nous envisagerons que la méthode de
débit puisse être exécutée par plusieurs thread.

1. class Program
2. {
3. static void Main(string[] args)
4. {
5. Thread[] threads = new Thread[10];
6. CompteBancaire compte = new CompteBancaire(1000);
7. for (int i = 0; i < 10; i++)
8. {
9. Thread t = new Thread(new
ThreadStart([Link]));
10. threads[i] = t;
11. }
12. for (int i = 0; i < 10; i++)
13. {
14. threads[i].Start();
15. }
16. [Link]();
17. }
18. }
19.
20. class CompteBancaire
21. {
22. private Object thisLock = new Object();
23. private float solde;
24.
25. public CompteBancaire(int initial)
26. {
27. [Link] = initial;
28. }
29.
30. bool Debiter(float transaction)
31. {
32.
33. if (solde < 0)
34. {
35. throw new Exception("Solde négatif");
36. }
37.
38. //lock (thisLock)
39. {
40. if (solde >= transaction)
41. {
42. [Link]("Solde avant transaction:{0}", solde);
43. [Link]("Transaction:{0}", transaction);
44. solde = solde - transaction;
45. [Link]("Solde après transaction:{0}", solde);
46. return true;
47.
48. }
49. Else
50. {
51. return false;
52. }
53. }
54. }
55.
56. public void SimulationTransactions()
57. {
58. Random quantite = new Random();
59. for (int i = 0; i < 100; i++)
60. {
61. Debiter([Link](1, 100));
62. }
63. }
64. }

Nous avons volontairement placé la ligne 38 en commentaire. L'exécution d'un tel programme
provoquera la levée d'une exception alors qu'au premier abord, le solde devrait resté positif ou
nul du fait du test effectué à la ligne 40.
Alors que dans un thread, ce test a été effectué, alors que la ligne 44 n'est pas encore exécutée,
un autre thread peut avoir changé le contenu du solde et de ce fait le solde devient ensuite
négatif.

Il existe d'autres mécanismes de protection comme la classe monitor (lock en est une
utilisation simplifiée) et la classe mutex.

19.3. Utilisation des méthodes Invoke, BeginInvoke et EndInvoke.

Un exemple classique d'utilisation de la méthode Invoke est celui lié à la présence d'une barre
de progression dans un formulaire windows qui indique l'état d'avancement d'un traitement
exécuté dans un thread secondaire.
Nous allons dans une première démarche tenter l'accès à la barre de progression à partir du
thread secondaire sans aucune précausion. Nous ajouterons dans le formulaire un panel dont
nous changerons la couleur de fond ainsi qu'un label dont le texte sera changé en fonction de
l'état d'exécution du thread
Voici la partie du code relative au thread:

1. private void WorkerThread()


2. {
3. Try
4. {
5. [Link] = [Link];
6. [Link] = "En cours";
7.
8. for (int i = 0; i < 100; i++)
9. {
10. [Link](100);
11. [Link] = i;
12. }
13. }
14. Finally
15. {
16. [Link] = [Link];
17. [Link] = "Arrêté";
18. }
19. }

L'exécution du programme et notamment l'exécution du thread provoquera une exception au


code de la ligne 6. En voici le contenu:

Nous devrons donc prévoir une technique permettant la modification du label à partir du
thread à l'origine de la création de la ressource. L'utilisation de la méthode Invoke peut
répondre à ce besoin.

private void WorkerThread()


{
Try
{
[Link] = [Link];
[Link]((MethodInvoker) delegate {[Link]="En cours";});

for (int i = 0; i < 100; i++)


{
[Link](100);
[Link]((MethodInvoker) delegate {[Link] = i;});
}
}
Finally
{
[Link] = [Link];
[Link](new MethodInvoker( ()=> {[Link] = "Arrêté"; }));
}
}

La méthode Invoke est bloquante tandis que la méthode BeginInvoke est non bloquante. Elle
est associée à EndInvoke permettant la fin de l'exécution de la méthode asynchrone.

19.4. Utilisation de [Link] (Framework 4.5)

La mise en place d'un thread n'est pas aisée pour l'exécution d'une méthode de façon
asynchrone. Reprenons l'exemple du paragraphe traitant de Thread et adaptons le.

1. namespace WorkerThread
2. {
3. class Program
4. {
5. [STAThread]
6. static void Main(string[] args)
7. {
8. var task = new Task<int>([Link]);
9. [Link]();
10. [Link]("Tâche lancée.");
11.
12. [Link]("Retour: {0}",[Link]());
13. [Link]();
14. }
15. }
16.
17. class class1
18. {
19. public static int Compteur;
20. public static bool Sortie;
21. public static int traitement()
22. {
23. Try
24. {
25. Compteur = 0;
26. Sortie = false;
27. while (Compteur < 500)
28. {
29. Compteur++;
30. [Link](10);
31. }
32. [Link]("thread terminé");
33.
34. }
35. catch (ThreadAbortException ex)
36. {
37. [Link]("Sortie forcée du thread");
38. }
39. return Compteur;
40.
41. }
42. }
43. }
Une autre façon de pouvoir démarrer le thread est d'utiliser la technique suivante:
var task=[Link]<int>([Link]);

La classe Task comprend d'autres méthodes statiques tels que:

1. [Link] permettant un effet identique à [Link]


2. [Link] et [Link] permettant à un thread de s'exécuter automatiquement
lorsque une ou plusieurs autres threads passés en argument sont terminés. Cette solution
permettra la chaînage automatique des threads sans trop se soucier des techniques de
synchronisation.
3. [Link] et [Link] permettant l'attente de fin d'exécution d'un des threads ou
de l'ensemble des threads.

Le chaînage des méthodes asynchrones nécessite parfois de récupérer les valeurs de retour de
thread précédemment exécutés. ContinueWith permet cette technique.

1. namespace WorkerThread
2. {
3. class Program
4. {
5. [STAThread]
6. static void Main(string[] args)
7. {
8. var task=[Link]<int>([Link]).ContinueWith<int>([Link]
9. [Link]("Tâche lancée.");
10.
11. [Link](task);
12. [Link]("fin:{0}",[Link]);
13. [Link]();
14. }
15. }
16.
17. class class1
18. {
19. public static int Compteur;
20. public static bool Sortie;
21. public static int SecondTraitement(Task<int>Traitement)
22. {
23. return [Link] * 10;
24. }
25. ....

19.5. Utilisation des mots clefs await et async (Csharp 5.0)

Reprenons le code suivant:

var task = new Task<int>([Link]);


[Link]();

Il serait souhaitable de pouvoir proposer dans nos classes l'exécution de méthodes


asynchrones sans que l'utilisateur des méthodes n'ait à devoir utiliser la classe générique Task.
Nous allons donc créer nos méthodes asynchrones au moyen du mot clef async. Si nous
reprenons notre méthode traitement, nous allons créer une méthode TraitementAsync qui aura
la structure suivante:
1. public static async void TraitementAsync()
2. {
3. await [Link]<int>([Link]);
4. }

Une méthode marquée du modifieur async retourne un type void, un type Task ou Task<T>.
Le modifieur indique que la méthode peut exécuter son code de manière asynchrone à
condition que la méthode comprenne dans son code le mot clef await.
Un exemple concret est le chargement du contenu d'une page web sous la forme d'une chaîne
de caratères. Ce chargement pouvant être lent, nous pouvons envisager l'exécution du code lié
au clic sur le bouton permettant le chargement de cette page de façon asynchrone.

1. private async void button1_Click(object sender, EventArgs e)


2. {
3. [Link] = false;
4. var Client = new WebClient();
5. string url = "[Link]
6. string data = await [Link](url);
7. [Link] = data;
8. [Link] = true;
9. }

Le code lié au bouton s'exécutera de façon asychrone et de façon non bloquante tandis que
l'on attendra le chargement des données liées à l'url pour mettre à jour la textbox et rendre le
bouton de nouveau accessible. Le formulaire graphique récupèrera tout de suite la main sans
attendre la fin de l'exécution du code.

Avant l'apparition de cette technique, l'exécution asynchrone existait déjà mais de façon
moins simple. Il suffisait d'appeler une méthode asynchrone en lui renseignant une méthode
de callback pour finalement mettre à jour la textbox.

1. private async void button2_Click(object sender, EventArgs e)


2. {
3. [Link] = false;
4. var Client = new WebClient();
5. string url = "[Link]
6. [Link] += Client_DownloadStringCompleted;
7. [Link](new Uri(url),Client);
8.
9. }
10.
11. void Client_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs
e)
12. {
13. if ([Link] == false && [Link] == null)
14. {
15. [Link] = [Link];
16. }
17. ((WebClient)[Link]).Dispose();
18. [Link] = true;
19.
20. }
19.6. Attributs caller information

En utilisant les attributs d'information de l'appelant, nous pouvons obtenir des informations
sur l'appelant à une méthode. Nous pouvez obtenir le chemin du fichier de code source, le
numéro de ligne dans le code source et le nom du membre de l'appelant. Cette information est
utile pour le traçage, le débogage et la création d'outils de diagnostic.

Dans les try-catch, nous pourrions tracer les erreurs et les afficher de sorte d'obtenir de plus
amples informations pour un debbugage ultérieur.

1. class GestionnaireDesLogs
2. {
3. static public void LogException(Exception exc,
4. [CallerMemberName] string memberName = "",
5. [CallerFilePath] string sourceFilePath = "",
6. [CallerLineNumber] int sourceLineNumber = 0)
7. {
8. [Link]([Link]("Date: {0}", [Link]));
9. [Link]([Link]("Exception: {0}", [Link]));
10. [Link]([Link]("Occured in: {0}", memberName));
11. [Link]([Link]("source file path: {0}", sourceFilePath));
12. [Link]([Link]("source line number: {0}",
sourceLineNumber));
13. [Link]();
14. }
15.
16. }

20. Les nouveautés dans le Csharp 6.0.

20.1. Initialiseurs de propriétés

Nous avons vu comme nouveauté dans la version 3 du csharp l’utilisation possible des
propriétés automatiques dont voici en rappel la syntaxe.

1. class CompteBancaire
2. {
3. public string NumeroCompte { get; set; }
4. public float Solde { get; set; }
5. public CompteBancaire(string NumeroCompte)
6. {
7. [Link] = NumeroCompte;
8. [Link] = 0;
9. }
10. }

Les propriétés sont initialisées dans le constructeur. Avec la version 6 du Csharp, nous
pouvons envisager l’initialisation lors de la déclaration de la propriété. Prenons le cas dans
notre exemple de la propriété Solde.

1. class CompteBancaire
2. {
3. public string NumeroCompte { get; set; }
4. public float Solde { get; set; } = 0;
5. public CompteBancaire(string NumeroCompte)
6. {
7. [Link] = NumeroCompte;
8. }
9. }

20.2. Les propriétés automatiques en lecture seule

Pour rendre une propriété accessible en lecture seule, il suffirait de supprimer l’assesseur
set qui permettrait uniquement l’accès en écriture en utilisant la technique précédemment
développée ou éventuellement dans le constructeur.
Enlever l’assesseur supprime l’accessibilité par tout objet mais aussi en interne.

1. public string NumeroCompte { get; set; }


2. public float Solde { get;}
3. public CompteBancaire(string NumeroCompte)
4. {
5. [Link] = NumeroCompte;
6. [Link] = 0;
7. }
8. public void Crediter(int montant)
9. {
10. [Link] += montant;
11. }

La compilation d’un tel code provoque une erreur au niveau de la ligne 10.

La solution apportée dans cette version du Csharp est d’utiliser le mot clef private lors de
la déclaration de la propriété au niveau de l’assesseur set.

1. public string NumeroCompte { get; set; }


2. public float Solde { get; private set; }

20.3. Initialisation des indexeurs et des dictionnaires

// Avant CSharp 6
var Dic1 = new Dictionary<int, string>
{
{ 1 , "un"},
{ 2 , "deux"},
{ 3 , "trois"},
};

// Avec CSharp 6
var Dic2 = new Dictionary<int, string>
{
[1] = "un",
[2] = "deux",
[3] = "trois"
};

Pour les indexeurs intégrés dans les classes, nous prendrons l’exemple d’un parc
automobile comprenant une collection de véhicules.

1. public class Vehicule{ public string NumChassis { get; set; } }


2.
3. public class ParcAutomobile
4. {
5. private Vehicule[] Flotte = new Vehicule[20];
6.
7. public Vehicule this[int i]
8. {
9. get { return Flotte[i];}
10. set
11. {
12. if (i < 20) Flotte[i] = value;
13. else throw new IndexOutOfRangeException();
14. }
15. }
16. }

L’instanciation d’une telle classe et l’accès à un des élements de la collection peut se faire
de la façon suivante suivant que l’on travaille avant la version 6 ou après la version 6

1. var G1 = new ParcAutomobile();


2. //Avant CSharp 6
3. G1[1] = new Vehicule();
4. // Avec CSharp 6
5. var G2 = new ParcAutomobile { [1] = new Vehicule() };

20.4. Initialisation des collections

Nous pouvons ajouter une méthode d’extension add dans les collections. Cette méthode
est appelée de façon implicite lors de l’ajout d’objets dans les collections. Dans notre
exemple nous commencerons par ajouter une méthode d’extension à la classe générique
List<T> adaptée à la classe « Vehicule »

1. public static class MethodesExtension


2. {
3. public static void Add(this List<Vehicule> Flotte, string NumChassis, int
Cylindree)
4. {
5. [Link](new Vehicule(NumChassis, Cylindree));
6. }
7. }

Une fois cette méthode Add ajoutée, nous imaginons un parc automobile étant
comprenant une flotte de véhicules sous forme d’une liste.
public class ParcAutomobile
{
public List<Vehicule> Flotte { get; set; }
}

ParcAutomobile p1 = new ParcAutomobile();


//Avant CSharp 6
[Link] = new List<Vehicule>()
{
new Vehicule("AAAA",1600),
new Vehicule("BBBB",2000)
};
//Avec CSharp 6
[Link] = new List<Vehicule>()
{
{"AAAA",1600},
{"BBBB",2000}
};

20.5. Les membres sous forme d’expression

1. // Avant CSharp 6
2. public string CompteResume1
3. {
4. get { return NumeroCompte + ":" + Solde; }
5. }
6. // Avec CSharp 6
7. public string CompteResume2 => NumeroCompte + ":" + Solde;

20.6. Les filtres d’exception

Les filtres d'exception sont des clauses qui déterminent quand une clause de capture
donnée devrait être appliquée. Si l'expression utilisée pour un filtre d'exception est vraie,
la clause catch effectue son traitement normal sur une exception. Si l'expression est
évaluée comme fausse, la clause catch est ignorée.

1. public static async Task<string> MakeRequest()


2. {
3. var client = new [Link]();
4. var streamTask = [Link]("[Link]
5. try
6. {
7. var responseText = await streamTask;
8. return responseText;
9. }
10. catch ([Link] e) when
([Link]("301"))
11. {
12. return "Site Moved";
13. }
14. catch ([Link] e) // Avant Csharp 6
15. {
16. return "Site Moved";
17. }
20.7. Await dans les blocs catch et finally

La version 5 du Csharp présente des limitations quant aux endroits où l’on peut utiliser
l’expression await. C’est le cas des blocs catch et finally. Cette situation n’existe plus
dans le Csharp version 6.

20.8. using static

« using static » permet d’importer les méthodes statiques d’une simple classe. L’accès
aux différents membres statiques ne nécessite plus de renseigner le nom de la classe.

1. using static [Link];


2. public void SayHello()
3. {
4. WriteLine("Hello World");
5. }

20.9. Opérateur nameof

L’opérateur « nameof » permet d’évaluer le nom d’un symbole. Cet opérateur est utile
lorsque l’on souhaite obtenir le nom d’une variable, d’un membre ou d’une propriété.
Avant cette fonctionnalité du Csharp 6, cette information devait être « softcodée » sous
forme d’une chaîne de caractères.

1. ParcAutomobile G3 = null
2. Vehicule v1 = G3?[1]
3. [Link](nameof(v1))
4. [Link](nameof([Link]))

20.10. Opérateur de nullité conditionnel « ?. » et « ?[] »

L’accès à un membre d’une classe à partir d’une référence qui est nulle provoque une
levée d’exception.

1. Vehicule v1 = null
2. [Link] = "1000"

Nous pouvons vérifier avant tout accès que la référence n’est pas nulle pour éviter la
levée de telles exceptions.

1. CompteBancaire cb1 = null;


2. //Avant CSharp 6
3. float?solde1 = cb1!= null ? (float?)[Link] : null;
4. //Avec CSharp 6
5. var solde2=cb1?.Solde;

Nous pouvons envisager, en utilisant l’opérateur ??, d’assigner une valeur par défaut dans
le cas où la référence est nulle.

1. //Avec CSharp 6
2. var solde2=cb1?.Solde;
3. var solde3 = cb1?.Solde ?? -1;

Nous retrouvons l’opérateur de nullité conditionnelle pour les accès aux tableaux et les
délégués.

1. ParcAutomobile G3 = null
2. Vehicule v1 = G3?[1]
3.
4. if (myDelegate?.Invoke(args) ?? false) { … }

20.11. Chaînes interpolées

1. //Avant CSharp 6
2. public override string ToString()
3. {
4. return [Link]("Le vehicule {0} a pour cylindree {1}", Identifiant,
Cylindree);
5. }
6. //Avec CSharp 6
7. public override string ToString()
8. {
9. return $"Le vehicule {Identifiant} a pour cylindree {Cylindree}";
10. }

Dans l’usage des chaînes interpolées, les chaînes sont formatées en utilisant la culture
courante. Nous pouvons néamoins utiliser la notion de « FormattableString » si nous
souhaitons travailler avec une culture différente.
Prenons pour bien comprendre la gestion du compte bancaire avec un solde exprimé en
flottant. L’affichage du solde en fonction de la culture pourrait fournir un point ou une
virgule comme séparateur décimal.

1. public override string ToString()


2. {
3. FormattableString str = $"Le Compte {NumeroCompte} a pour solde {Solde}";
4. var gradeStr =
[Link]([Link]("en-uk"),
5. [Link], [Link]());
6. return gradeStr;
7. }
21. Les nouveautés dans le Csharp 7.0.

21.1. Les littéraux binaires

CSharp 7 introduit la possibilité de déclarer les nombres binaires sous le format suivant
int data = 0b1010;

21.2. Les séparateurs digitaux

Pour apporter une meilleure lisibilité dans la réprésentation des nombres binaires,
hexadécimaux et décimaux, il est possible d’utiliser le caractère souligné comme
séparateur.
1. int data1 = 123_456_789
2. int data2 = 0xAB_CD_EF
3. int data3 = 0b1010_1011_1100_1101_1110_1111

21.3. Membres sous forme d'expression

Le CSharp 6 avait apporté cette fonctionnalité pour les membres, index, propriétés et
méthodes. Le CSharp 7 l’ajoute pour les constructeurs, destructeurs et exceptions.

1. class TemporaryFile
2. {
3. public TemporaryFile(string fileName) => File = new FileInfo(fileName);
4.
5. ~TemporaryFile() => Dispose();
6. FileInfo _File;
7. public FileInfo File
8. {
9. get => _File;
10. private set => _File = value;
11. }
12. void Dispose() => File?.Delete();
13. }

21.4. Les fonctions locales

• Les fonctions locales n'autorisent pas l'utilisation d'un modificateur d'accessibilité


(public, privé, protégé).
• Les fonctions locales ne supportent pas la surcharge. Vous ne pouvez pas avoir
deux fonctions locales dans la même méthode avec le même nom, même si les
signatures ne se chevauchent pas.
• Le compilateur émettra un avertissement pour les fonctions locales qui ne sont
jamais invoquées.
• Les fonctions locales peuvent accéder à toutes les variables dans la portée, y
compris les variables locales. Ce comportement est identique avec les expressions
lambda définies localement, sauf que les fonctions locales n'attribuent pas un
objet qui représente la fermeture, comme les expressions lambda définies
localement.
• Les fonctions locales sont destinées à l'ensemble de la méthode, qu'elles soient
invoquées avant ou après leur déclaration.
1. static bool EstUnPalindrome(string word)
2. {
3. if (word == null)
4. throw new ArgumentNullException(nameof(word));
5.
6. if ([Link] < 2)
7. return true;
8.
9. return EstUnPalidrome(0, [Link] - 1);
10.
11. bool EstUnPalidrome(int IndiceBas, int IndiceHaut)
12. {
13. if (IndiceBas >= IndiceHaut)
14. return true;
15.
16. if ([Link](word[IndiceBas]) !=
17. [Link](word[IndiceHaut]))
18. return false;
19.
20. return EstUnPalidrome(IndiceBas + 1, IndiceHaut - 1);
21. }
22. }

21.5. Les variables déclarées avec le mot clef out

Nous avons vu dans les versions précédentes du CSharp que l’utilisation du mot clef ref
nous permettait de faire passer des arguments de type valeur à des méthodes sous forme
de référence. Un des points négatifs du mot clef ref était la nécessité de devoir initialiser
la variable. Le mot clef out nous permettait de nous dispenser de cette initialisation.
En CSharp 7 il n’est plus nécessaire de déclarer ces variables avant. Nous pouvons le
faire lors du passage des arguments

1. //Avant CSharp 7
2. float data2;
3. Getdata(out data2);
4. //Avec CSharp 7
5. Getdata(out float data3);
6.
7. static void Getdata(out float data)
8. {
9. data = 10.0F;
10. }

Comme il n’est plus nécessaire de déclarer ces variables au préalable, nous pouvons
penser qu’il est possible d’utiliserv le mot clef var, le type de la variable étant alors lié à
la valeur renseignée lors de la déclaration de la fonction

1. Getdata(out var data3);

21.6. Le mot clef ref pour les retours et les locaux

Il est possible en C# de faire passer des variables de type valeur par référence en utilisant
le mot clef ref ou le mot clef out. Le Csharp 7 permet retourner des références.
1. static ref int Getdata(int [] numbers, int number)
2. {
3. for (int i=0;i<[Link];i++)
4. {
5. if (numbers[i]==number)
6. {
7. return ref numbers[i];
8. }
9. }
10. throw new Exception("number not found");
11. }

Attention que l’utilisation de ce mot clef dans les déclarations de variables locales est très
limité.

1. class Classe1
2. {
3. //Déclarations en erreur
4. ref string Nom1;
5. ref string Nom2 { get; set; } //Propriétés auto implémentées
6. //Déclaration acceptée
7. string Nom3 = "Dupond Albert";
8. ref string Nom4 { get { return ref Nom3; } }
9. }

21.7. Les tuples

Dans les versions antérieures du CSharp, nous avons parfois été confronté au besoin de
retourner plusieurs valeurs vers la fonction appelante. Nous avions comme solution la
création d’un objet complexe comprenant ces valeurs ou encore l’utilisation des mots
clefs ref ou encore out.
La version 7 du CSharp nous permet maintenant cette fonctionnalité de façon simplifiée.

1. public (string Marque, int Cylindree) TrouverVehicule(string NumChassis)


2. {
3. for (int i= 0; i < [Link];i++)
4. {
5. if (Flotte[i].NumChassis==NumChassis)
6. {
7. return (Flotte[i].Marque, Flotte[i].Cylindree); //tuple littéral
8. }
9. }
10. throw new Exception("Numero de chassis absent");
11. }

Dans la déclaration de la méthode nous pouvons utiliser une forme simplifiée :

public (string , int ) TrouverVehicule(string NumChassis)

Si nous souhaitons travailler avec des noms différents, nous pouvons utiliser la syntaxe
suivante :

1. public (string MaMarque , int MaCylindree ) TrouverVehicule(string NumChassis)


2. {
3. for (int i= 0; i < [Link];i++)
4. {
5. if (Flotte[i].NumChassis==NumChassis)
6. {
7. return (MaMarque: Flotte[i].Marque, MaCylindree:
Flotte[i].Cylindree); //élements nommés dans un tupple littéral
8. }
9. }
10. throw new Exception("Numero de chassis absent");
11. }

Les Tuples sont des types de valeur, et leurs éléments sont simplement des champs
publicitaires et mutables. Ils ont une égalité de valeur, ce qui signifie que deux tuples sont
égaux (et ont le même code hash) si tous leurs éléments sont par paires égaux (et ont le
même code hash).

Nous voyons comment les tuples sont construits. Nous allons voir comment nous
pouvons les déconstruire pour en récupérer des variables « simples »

1. (string Marque, int Cylindree)=[Link]("AAAA")


2. (var Marque1, var Cylindree1) = [Link]("AAAA")
3. var(Marque2,Cylindree2)= [Link]("AAAA")
4. string Marque3
5. int Cylindree3
6. (Marque3,Cylindree3)= [Link]("AAAA")

Le mécanisme de déconstruction peut aller bien au-delà de l’application aux Tuples.


Reprenons le cas de nous classe Véhicules. Nous souhaiterions pouvoir utiliser la syntaxe
suivante :

1. var v10 = new Vehicule { Marque = "Ford", Cylindree = 1600 }


2. var (Marque5, Cylindree5) = v10

Cette fonctionnalité est envisageable si nous implémentons la méthode d’extension


Deconstruct pour notre classe Véhicule

1. public static class MethodesExtension


2. {
3. public static void Deconstruct(this Vehicule vh, out string Marque, out int
Cylindree)
4. {
5. Marque = [Link];
6. Cylindree = [Link];
7. }
8. }

Le CSharp 4 avait déjà implémenté l’utilisation dans le framework correspondant d’une


structure permettant une gestion similaire sous la forme :

1. public Tuple<string, string> GetPersonne()


2. {
3. return new Tuple<string, string>("Dupond", "Bernard");
4. }
21.8. Le filtrage par motif (Pattern matching)

C # 7.0 introduit la notion de motifs qui, abstraitement parlant, sont des éléments
syntaxiques qui peuvent tester qu'une valeur a une certaine «forme» et extraire des
informations de la valeur quand elle l'est. Le CSharp s’appuie sur les opérateurs is et
when pour nous apporter cette fonctionnalité.
Il y a 3 types de patterns avec lesquels nous pouvons valider une séquence :

• Des constantes
• Des types
• Des types inférés

Commençons pas l’utilisation du filtrage adapté à l’instruction switch… case

1. public (int nbvoitures,int nbutilitaires)Count()


2. {
3. int nbvoitures=0, nbutilitaires=0;
4. for (int i=0; i<[Link]; i++)
5. {
6. switch(Flotte[i])
7. {
8. case Voiture r:
9. nbvoitures++;
10. break;
11. case Utilitaire u:
12. nbutilitaires++;
13. break;
14. default:
15. break;
16. }
17. }
18. return (nbvoitures, nbutilitaires);
19. }

Imaginons que l’on souhaite compter les véhicules en fonction de deux marques utilisées
dans le parc automobile. Nous utiliserons alors l’opérateur when lié au « case »

1. public (int Volvo,int Renault)Count()


2. {
3. int Volvo=0, Renault=0;
4. for (int i=0; i<[Link]; i++)
5. {
6. switch(Flotte[i])
7. {
8. case Vehicule v when [Link]=="Renault":
9. Renault++;
10. break;
11. case Vehicule v when [Link]=="Volvo":
12. Volvo++;
13. break;
14. default:
15. break;
16. }
17. }
18. return (Volvo, Renault);
19. }
En dehors du « case », nous pouvons utiliser le même principe avec l’instruction is. Nous
adapterons le code précédent en utilisant une instruction if . Nous prendrons le cas où
nous désirons compter les véhicules de type utilitaires et d’une marque déterminée.

1. public int CompterVolvo()


2. {
3. int Compteur = 0;
4. for (int i = 0; i < [Link]; i++)
5. {
6. if (Flotte[i] is Utilitaire u && [Link] == "Volvo")
7. {
8. Compteur++;
9. }
10. }
11. return Compteur;
12. }

Vous aimerez peut-être aussi