Introduction à C# et .NET Framework
Introduction à C# et .NET Framework
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.
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.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:
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.
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.
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:
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.
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.
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.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:
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.
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.
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.
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. }
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.
[Link](tabsec);
[Link](tabsec[2]);
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. }
deux
trois
un
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. }
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"}};
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. }
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. }
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]( ));
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.
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 :
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é.
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 :
Nous remarquerons à ce niveau ci une différence avec le C++ car l’héritage ne comprend plus de
modificateur d’accessibilité.
En C#, la déclaration des méthodes comprend le modificateur d’accès. Nous retrouvons la syntaxe
suivante lors de la déclaration :
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 }
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.
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:
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:
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#.
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.
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 }
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.
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.
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
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 }
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.
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
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).
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:
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
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.
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.
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]()'
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);
}
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 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.
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.
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().
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.
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( ):
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:
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
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.
(*) 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.
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 }
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.
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.
Commentaires:
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.
• 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.
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.
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:
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.
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.
Pour bien comprendre l’utilité des méthodes anonymes, nous allons reprendre l’exemple
traitant de l’utilisation des délégués.
Pour rappel, l’utilisation d’un délégué nous oblige à respecter trois étapes bien distinctes :
2- La création de la fonction.
static bool CompInt(object x, object y)
{
return ((int)x<(int)y);
}
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 :
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 :
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 :
2- La création de la fonction.
static bool CompInt(object x, object y)
{
return ((int)x<(int)y);
}
8.1. Introduction.
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.
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));
-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.
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 :
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 :
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 }
-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 }
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 :
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.
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.
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 }
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 }
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 }
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]();
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:
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:
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...
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.
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.
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:
tri(clientBar,comp);
tri(clientbar,comp);
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.
Nous retrouverons également l'emploi du mot clef Action ne retournant aucun type
contrairement au mot clef Func
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.
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:
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
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.
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:
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; }
}
var classe2 = [Link](x => [Link] == 1).OrderBy(x => [Link]).Select(x => x);
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 }
Tous les paramètres facultatifs doivent apparaître après les paramètres requis. La syntaxe
suivante ne serait donc pas autorisée:
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 }
Ou alors se réferer au paragraphe suivant qui offre une autre solution en proposant 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.
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.
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.
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:
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éé.
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]
// Contravariance. L'argument renseigné dans le délégué est un type string tandis que
la méthode SetObject renseigne un type object.
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:
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.
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:
Le type T est bien utilisé dans la déclaration des arguments de la méthode Compare.
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).
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.
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. }
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.
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:
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.
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.
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]);
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. ....
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.
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.
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. }
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. }
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.
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.
// 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.
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
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 »
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; }
}
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;
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.
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.
« 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.
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]))
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.
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) { … }
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.
CSharp 7 introduit la possibilité de déclarer les nombres binaires sous le format suivant
int data = 0b1010;
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
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. }
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
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. }
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.
Si nous souhaitons travailler avec des noms différents, nous pouvons utiliser la syntaxe
suivante :
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 »
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
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 »