C#/.NET
C# / .NET
support de cours
ISTIA
2012-2013
Bertrand Cottenceau - bureau 311 ISTIA
bertrand.cottenceau@univ-angers.fr
1
C#/.NET
Références bibliographiques :
Introduction au langage C# de Serge Tahé
disponible sur http://tahe.developpez.com/dotnet/csharp/
Transparents du cours de H.Mössenböck, University of Linz
http://www.ssw.uni-linz.ac.at/Teaching/Lectures/CSharp/Tutorial/
C# 4.0 in a Nutshell de Joseph et Ben Albahari (O'Reilly)
C# 3.0 Design Patterns de Judith Bishop (O'Reilly)
2
C#/.NET
1. Introduction
Le langage C# (C Sharp) est un langage objet créé spécialement pour le framework Microsoft .NET. L'équipe
qui a créé ce langage a été dirigée par Anders Hejlsberg, un informaticien danois qui avait également été à l'origine
de la conception du langage Delphi pour la société Borland (évolution objet du langage Pascal).
Le Framework .NET est un environnement d'exécution (CLR Common Language Runtime) ainsi qu'une
bibliothèque de classes (plus de 2000 classes). L'environnement d'exécution (CLR) de .NET est une machine
virtuelle comparable à celle de Java.
Le runtime fournit des services aux programmes qui s'exécutent sous son contrôle : chargement/exécution,
isolation des programmes, vérification des types, conversion code intermédiaire (IL) vers code natif, accès aux
métadonnées (informations sur le code contenu dans les assemblages .NET), vérification des accès mémoire
(évite les accès en dehors de la zone allouée au programme), gestion de la mémoire (Garbage Collector), gestion
des exceptions, adaptation aux caractéristiques nationales (langue, représentation des nombres), compatibilité
avec les DLL et modules COM qui sont en code natif (code non managé).
Les classes .NET peuvent être utilisées par tous les langages prenant en charge l'architecture .NET. Les
langages .NET doivent satisfaire certaines spécifications : utiliser les mêmes types CTS (Common Type System),
les compilateurs doivent générer un même code intermédiaire appelé MSIL (Microsoft Intermediate Language).
Le MSIL (contenu dans un fichier .exe) est pris en charge par le runtime .NET qui le fait tout d'abord compiler
par le JIT compiler (Just In Time Compiler). La compilation en code natif a lieu seulement au moment de
l'utilisation du programme .NET.
Définir un langage .NET revient a fournir un compilateur qui peut générer du langage MSIL. Les
spécifications .NET sont publiques (Common Language Specifications) et n'importe quel éditeur de logiciel peut
donc concevoir un langage/un compilateur .NET. Plusieurs compilateurs sont actuellement disponibles :
C++.NET (version Managée de C++), VB.NET, C#, Delphi, J#
Lors du développement d'applications .NET, la compilation du code source produit du langage MSIL contenu
dans un fichier .exe. Lors de la demande d'exécution du fichier .exe, le système d'exploitation reconnaît que
l'application n'est pas en code natif. Le langage IL est alors pris en charge par le moteur d'exécution du
framework .NET qui en assure la compilation et le contrôle de l'exécution. Un des points importants étant que le
runtime .NET gère la récupération de mémoire allouée dans le tas (garbage collector), à l'instar de la machine
virtuelle Java.
"En .NET, les langages ne sont guères plus que des interfaces syntaxiques vers les bibliothèques de classes."
Formation à C#, Tom Archer, Microsoft Press.
On ne peut rien faire sans utiliser des types/classes fournis par le framework .NET puisque le code MSIL
s'appuie également sur les types CTS. L'avantage de cette architecture est que le framework .NET facilite
l'interopérabilité (projets constitués de sources en différents langages). Avant celà, faire cohabiter différents
langages pour un même exécutable imposait des contraintes de choix de types “communs” aux langages ainsi que
de respecter des conventions d'appel de fonctions pour chacun des langages utilisés.
Remarque (particularité de C++.NET) : la version .NET de C++ a la particularité de permettre à l'utilisateur
de générer soit du code natif soit du code managé. Avec Visual C++ Express, le langage C++ a été complété
pour permettre la gestion managée de la mémoire allouée dans le tas.
3
C#/.NET
2. Les héritages du langage C
Le C# reprend beaucoup d'éléments de syntaxe du C. Partant du langage C, beaucoup d'éléments
de syntaxe sont familiers :
- structure des instructions similaires (terminées par ;) : déclaration de variables, affectation, appels
de fonctions, passage des paramètres, opérations arithmétiques
- blocs délimités par {}
- commentaires // ou /* */
- structures de contrôle identiques : if/else, while, do/while, for( ; ; )
- portée des variables : limitée au bloc de la déclaration
En C#, on peut déclarer une variable n'importe où avant sont utilisation. Ce n'est pas nécessairement au début
d'une fonction.
using System;
namespace ConsoleApplication
{
class Program
{
static void Main(string[] args)
{
for (int i = 0; i < 4; i++)
{
Console.WriteLine("i={0}", i);
}
}
}
}
2.1. Premier exemple console
Tout ne peut pas être expliqué dans un premier temps. System.Console est la classe de gestion des I/O en
mode console.
Point d'entrée et entrées/sorties en mode console
La fonction principale (point d 'entrée) est la fonction statique void Main(...). En mode console, les
entrées sorties (saisies clavier + affichages) se font grâce à la classe Console.
class Program
{
public static void Main()
{
int var=2;
string str="toto";
Console.WriteLine("var= {0} str = {1}", var, str);
str = Console.ReadLine();
Console.WriteLine("Ligne lue = {0}", str);
}
}
Un second exemple où l'on voit les similitudes avec la syntaxe du C.
using System;
namespace ConsoleApplication
{
class Program
{
4
C#/.NET
static void Main(string[] args) // Point d'entrée
{
int var = 2;
for (int i = 0; i < 5; i++)
{
var += i;
Console.WriteLine("i={0} var={1}", i, var);
}
float f=2F;
while (f < 1000F)
{
f = f * f;
Console.WriteLine("f={0}", f);
}
}
}
i=0 var=2
i=1 var=3
i=2 var=5
i=3 var=8
i=4 var=12
f=4
f=16
f=256
f=65536
Appuyez sur une
touche pour
continuer...
}
Les arguments de la ligne de commande
Lorsque l'on exécute un programme console depuis l'invite de commandes, on peut passer des paramètres qui
sont reçus en arguments de la fonction statique Main().
E:\Enseignement\C#>Prog Toto 23 Texte
Programme appelé avec les paramètres suivants:
argument n° 0 [Toto]
argument n° 1 [23]
argument n° 2 [Texte]
static void Main(string[] args)
{
Console.WriteLine("Programme appelé avec les paramètres suivants:");
for (int i = 0; i < args.Length; i++)
{
Console.WriteLine("argument n° {1} [{0}]", args[i],i);
}
}
2.2. Types primitifs du C# (alias de types .NET)
Le framework .NET fournit les types utilisables, et ce quel que soit le langage utilisé (VB.NET, C#, C++.NET).
Dans C#, les types élémentaires sont nommés comme suit
char : caractère Unicode 16bits
sbyte/byte : entier 8 bits signé/non signé
short/ushort : entier 16 bits signé/non signé
int/uint : entier 32 bits signé/non signé,
long/ulong : entier 64 bits signé/non signé
float : flottant 32 bits,
double : flottant 64 bits ,
decimal (128 bits) : entier multiplié par une puissance de 10
string : chaîne de caractères. Type référence qui s'utilise comme un type valeur.
2.3. Littérals (constantes) / Format des constantes
En C#, les constantes numériques peuvent presque être considérées comme typées. Selon la notation utilisée
5
C#/.NET
pour décrire une constante, le codage n'utilise pas le même nombre de bits.
Constantes entières
int x=10;
int x = 0x2F;
long l= 10000L;
// x ppv 10 (base 10)
// x ppv (2F)h
// le L signifie que la constante est sur 64 bits
Constantes réelles
Par défaut, les constantes réelles sont codées sur 64 bits. Pour que les constantes réelles soient codées sur 32 bits,
il faut ajouter un suffixe F. Pour les constantes décimales, il faut ajouter un suffixe m.
double
float
float
double
float
decimal
d1=10.2;
f1=10.2;
f2=10.2F;
d2=1.2E-2;
f3 = 3.1E2F;
d4=2.5m;
//OK
//Error
// OK
// d2 = 0,012
// f3=310
Constantes chaînes de caractères
string
s1=”abc”;
Chaîne verbatim : lorsque la chaîne est préfixée par @, aucun des caractères entre les guillemets n'est interprété.
Pour représenter les “ dans une telle chaîne, il faut les doubler
string
string
s2=@”a\nebc”;
s3=@”a\n””ebc”;
// s ppv a\n”ebc
Caractères de contrôle
\n (newline) \t (horizontal tab) \v (vertical tab) \b (backspace) \r (carriage return) \f
(form feed) \\ (backslash) \' (single quote) \” (double quote) \0 (caractere de valeur
0).
2.4. Dépassements
Par défaut, les dépassements numériques sont silencieux. On n'est pas informé si celà se produit.
On peut néanmoins tester si les opérations arithmétiques ne conduisent pas à des dépassements, ou si les casts
sont dégradants (est-ce que la conversion long → int est sans perte de données). Les opérations réalisées dans le
bloc checked conduisant à un dépassement déclenchent une exception (cf. Exceptions)
try
{
checked
{
long l = 1000;
l = l * l;
l = l * l;
l = l * l;
Console.WriteLine(l);
}
}
catch (Exception e)
{
Console.WriteLine(e);
}
6
C#/.NET
2.5. Les fonctions et les passages de paramètres (ref, out)
En C#, les fonctions sont nécessairement définies au sein de types structurés (struct ou class). Il n'y a pas
réellement de fonction “globale” au sens du C. Néanmoins, les fonctions membres suivent la même logique de
définition et d'appel que les fonctions du C. La principale différence est que le compilateur C# n'a pas besoin de
fichier de prototypes de fonction (les fichiers .h header du C). Il retrouve seul la signature de la fonction à
l'intérieur de données de description de type. La définition des fonctions suffit. Il n'y a donc pas de fichiers
d'entête (.h) ce qui élimine beaucoup de problèmes de compilation.
En C#, il n'y a pas de prototype pour les fonctions (différent du C)
Le passage des paramètres se fait par valeur (comme en C)
using System;
namespace ConsoleApplication
{
class Program
{
static int f1(int param)
{
return 2*param;
}
static void Main(string[] args)
{
Console.WriteLine("f1(3)={0}", f1(3));
}
}
}
Les paramètres modifiables : mots clé ref et out
Par défaut, les paramètres sont passés par valeur, comme en C. Celà signifie que la fonction reçoit une
copie du paramètre passé à l'appel. Les éventuelles modifications du paramètre dans la fonction n'ont
aucune incidence sur la valeur du paramètre fourni lors de l'appel de la fonction.
Passage par valeur
class Program
{
static int f1(int param) // param est initialisé à partir de x
{
param++;
// param est incrémenté, mais sans effet sur x
}
static void Main(string[] args)
{
int x=2;
f1(x);
Console.WriteLine("x={0}", x);
}
// x vaut toujours 2
}
Le C# introduit des mots clés pour que des paramètres d'appel puissent être modifiables dans une
fonction (ca reprend l'idée du Pascal avec le mot clé var ou des références en C++). Les mots clés
utilisés en C# sont
ref : indique que le paramètre formel est un alias du paramètre d'appel. C'est à dire c'est la
même variable mais avec un autre nom localement.
out : idem. Ce paramètre n'est pas utilisé en lecture, seulement en écriture.
7
C#/.NET
using System;
namespace ConsoleApplication
{
class Program
{
static void Inc(ref int EntreeSortie) // le paramètre est une entrée sortie
{
// paramètre en lecture écriture
EntreeSortie++;
}
static void Reset(out int Sortie)
{
Sortie=0;
}
// le paramètre est en écriture seulement
static void Main(string[] args)
{
int x = 2;
Inc(ref x);
// Il faut remettre le mot clé ref pour l'appel !
Console.WriteLine(x);
Reset(out x);
// Il faut remettre le mot clé out pour l'appel !
Console.WriteLine(x);
}
}
}
2.6.
Définition d'un type structuré (similaire aux structures du C)
Le mot clé struct permet la définition d'un type. Le type créé représente des variables structurées avec un ou
plusieurs champs de données.
Ci dessous, une variable de type PointS est une variable contenant deux champs entiers. Ces champs
s'appellent X et Y et peuvent être désignés à l'aide de la notation p.X et p.Y où p est une variable de type
PointS. C'est très proche des types structurés du langage C.
La nouveauté est que l'on doit préciser un niveau d'accessibilité (ici les champs sont publics).
using System;
namespace Exemple
{
struct PointS
// PointS est un type
{
public int X;
public int Y;
} // fin PointS
class Program
{
static void Main(string[] args)
{
PointS p1,p2; // p1 et p2 sont des variables de type PointS
p1.X = 3;
p1.Y = 4;
p2.X = 6;
p2.Y = 1;
}
}
}
8
C#/.NET
2.7. Type structuré (struct) avec des fonctions membres
Du fait de l'orientation objet du langage C#, on peut avoir des fonctions membres dans les types structurés, et
pas uniquement des données. Il suffit de définir des fonctions au sein des blocs décrivant le type structuré. Dès
lors, sur une variable, on peut appeler des fonctions rattachées à cette variable. La notation est homogène avec
l'accès aux champs de la structure. On remarquera d'ailleurs que les fonctions utilisées sont nécessairement
membres d'un type : soit struct soit class. De plus, on peut avoir plusieurs fonctions membres ayant le même
nom, mais des signatures différentes. C'est ce qu'on appelle la surcharge.
using System;
namespace Exemple
{
struct PointS
{
public int X;
public int Y;
public void SetXY(int pX, int pY)
{
X = pX;
Y = pY;
}
public int GetX()
// les deux fonctions membres GetX ont des signatures
{
// différentes
return X;
}
public void GetX(ref int pX)
{
pX = X;
}
public string ToString()
{
return "(" + X.ToString() + "," + Y.ToString() + ")";
}
} // fin PointS
class Program
{
static void Main(string[] args)
{
PointS p1 = new PointS();
// p1 est une variable de type PointS
PointS p2 = new PointS();
// p2 est une variable de type PointS
p1.SetXY(2, 3);
p2.SetXY(6, 1);
// appel de la fonction membre SetXY sur la variable p1
Console.WriteLine("p2.X={0}", p2.GetX());
Console.WriteLine("p1" + p1.ToString());
Console.WriteLine("p2" + p2.ToString());
}
}
Sortie console
p2.X=6
p1(2,3)
p2(6,1)
}
3. La programmation orientée objet en C#
3.1. Le paradigme objet en bref (vocabulaire)
La Programmation Orientée Objet (POO) est une façon de programmer et de décomposer des applications en
informatique. Un objet est une variable structurée contenant des champs mais aussi des fonctions membres.
Ceci est possible en C# mais ne l'est pas en langage C.
9
C#/.NET
Qu'est-ce qu'un objet ? C'est une variable structurée avec des fonctions membres. Les variables de la structure
PointS (vue précédemment) sont en quelque sorte déja des objets.
Les attributs : ce sont les champs de données d'un objet. C'est une des caractéristiques d'un objet. Ils décrivent
les données nécessaires pour décrire un objet. Par exemple, X et Y sont les attributs d'un objet de type PointS
Les méthodes : autre nom (utilisé en POO) pour les fonctions membres. Ces fonctions membres représentent
les mécanismes/ les fonctionnalités dont dispose un objet. C'est ce qu'il sait faire, ça décrit son comportement.
L'ensemble des méthodes d'un objet est aussi appelé l'interface d'un objet.
L'encapsulation : en POO, les attributs ne sont généralement pas accessibles directement (contrairement à X et
Y dans le type PointS). On va pouvoir modifier le niveau d'accès des membres (mots clés d'accessibilité) de façon
à ce que les données d'un objet (valeurs de ses attributs) ne soient pas accessibles directement, mais uniquement
grâce à ses méthodes. Le fait de cacher certains détails de fonctionnement d'un objet est appelé encapsulation.
Les mécanismes de manipulation des données d'un objet sont généralement cachés à l'utilisateur. Ceci de façon à
ce que l'utilisateur ne se concentre que sur les méthodes de l'objet (ce qu'il sait faire). Le gros intérêt de
l'encapsulation est que l'on peut très bien modifier le comportement interne d'un objet (par exemple certains
algorithmes) sans changer son comportement global vu de l'extérieur : l'objet rend les mêmes services mais avec
des algorithmes différents. On arrive ainsi à découpler certaines parties du logiciel.
Etat d'un objet : c'est la valeur de l'ensemble de ses attributs à un instant donné. Par exemple, l'état d'un objet
de type PointS est simplement la valeur des coordonnées.
3.2. Le mot clé class ( class vs struct)
Le C# (comme d'autres langages objet) utilise le mot clé class pour pouvoir définir d'autres types structurés
avec des méthodes (struct ne convient pas à tous les objets). Les types class sont plus riches que les types
struct (on peut dériver des classes) et conduisent également à un fonctionnement différent de la gestion de la
mémoire. On peut donc aussi définir un type class pour représenter des points du plan.
using System;
namespace TypeClass
{
class Program
{
class PointC
{
// donnés membres privées (non accessibles depuis l'extérieur de l'objet)
private int X;
private int Y;
// les attributs X et Y sont encapsulés
// les méthodes sont publiques
public void SetXY(int pX,int pY)
{
X=pX;
Y = pY;
}
public void GetXY(out int pX, out int pY)
{
pX = X;
pY = Y;
}
public string ToString()
{
return "(" + X.ToString() + "," + Y.ToString() + ")";
}
} // fin PointC
10
C#/.NET
static void Main(string[] args)
{
PointC p3 = new PointC();
PointC p4 = new PointC();
p3.SetXY(2, 3);
p4.SetXY(6, 1);
Console.WriteLine(p3.ToString());
Console.WriteLine(p4.ToString());
(2,3)
(6,1)
x=2 y=3
Appuyez sur une touche
pour continuer...
int x, y;
p3.GetXY(out x, out y);
Console.WriteLine("x={0} y={1}",x,y);
// p3.X = 4; // IMPOSSIBLE l'attribut X de l'objet p3 n'est pas accessible
}
}
}
Qu'est-ce que l'instanciation ? C'est le fait de créer une instance (ç-à-d un objet) d'un type class ou struct.
C'est donc le fait de créer une nouvelle variable objet. Un type classe décrit ce qu'ont en commun tous les objets
qui seront générés, ç-à-d leurs caractéristiques communes. Un objet est donc une instance particulière d'un
ensemble potentiellement infini d'objets ayant des traits communs : mêmes méthodes et mêmes attributs. En
revanche les objets n'ont pas tous le même état (les attributs n'ont pas tous les mêmes valeurs !)
Dans l'exemple précédent, p3 et p4 désignent deux instances (deux objets) de la classe PointC. Les deux objets
ont les mêmes attributs et les mêmes méthodes (les mêmes caractéristiques). Mais ils n'ont pas le même état.
3.3. Types valeurs / Types référence
En C#, selon le type d'une variable / d'un objet (rappelons qu'un objet n'est rien d'autre qu'une variable
structurée), l'allocation de mémoire est différent. L'allocation d'une variable n'est donc pas le simple fait du
programmeur (comme en C où l'on choisit ce que l'on met dans la pile (par défaut) et ce que l'on met dans le tas
(allocation dyamique par des fonctions spécifiques)).
En C#, selon le type d'une variable, celle-ci est réservée soit dans la pile soit dans le tas (mémoire allouée
dynamiquement). On distingue deux catégories de types : les types “valeur” et les types “référence”.
Les types “valeur” → alloués dans la pile
Les types “référence” → alloués dans le tas (allocation dynamique)
Quels sont les types “valeur” ? Tous les types numériques primitifs du C# (byte, char, long …), les
énumérations et les variables générées à partir d'un type struct. Le type structuré PointS est un type valeur.
Quand on crée une variable de type PointS, elle est réservée dans la pile. La mémoire des variables de type
valeur est libérée automatiquement à la fin du bloc de leur déclaration (comme les variables locales en C)
Quels sont les types “référence” ? Les types créés avec le mot clé class (ainsi que le type string).
Dans ce cas, la mémoire est allouée dynamiquement dans le tas quand on crée un objet (avec le mot clé new).
Quand on instancie une classe (ie qu'on crée un objet), on réserve de la mémoire dans le tas. On récupère une
référence (on peut voir ca comme un pointeur) sur l'emplacement réservé. Pour les types références, ce sont des
références qui permettent de désigner les objets.
Quand la mémoire est-elle désallouée ? : pour les variables de type valeur, à la fin du bloc de déclaration.
Pour les objets de type référence, quand il n'y a plus aucune référence sur un objet donné (il n'y a alors plus
aucun moyen d'y accéder). Dans ce cas, le Garbage Collector (un programme qui s'exécute périodiquement pour
libérer de la mémoire) récupère la mémoire de l'objet. Ca veut dire que la zone mémoire peut de nouveau être
allouée à un autre objet nécessaitant de la mémoire dans le tas. L'intérêt du C# par rapport au C/C++, est qu'on
ne peut donc pas oublier de désallouer de la mémoire (fuites mémoires). La désallocation n'est pas de notre
responsabilité.
11
C#/.NET
3.4. Distinctions Types valeur/Types référence concernant l'affectation
Cette distinction entre types “valeur” et types “référence” est essentielle en .NET. C'est un as^pect du système
qu'il faut vraiment bien comprendr. De plus, l'affectation n'a pas le même sens pour les variables de type valeur
et celles de type référence.
Affectation pour les types “valeur” Une variable de type valeur contient des données.
int i=18;
// i est une variable allouée dans la pile contenant l'entier signé 18 codé sur 32 bits
Remarque : même dans la situation ci-dessous, la variable est dans la pile.
int i = new int();
//i est néanmoins alloué dans la pile
Pour les variables de type valeur, l'affectation réalise l'affectation des valeurs.
int a=12,b=4;
a=b;
// a contient 12 et b contient 4
// a contient désormais la valeur 4.
// la valeur de b a été affectée à la variable a
Même pour les variables de type struct, l'affectation affecte la valeur, champs par champs. Dans l'exemple cidessous où PointS est le type struct défini précédemment, l'affectation p2=p1 réalise l'affectation attribut par
attribut.
p2=p1
équivaut à p2.X=p1.X et p2.Y=p1.Y (même si les champs X et Y étaient privés! )
static void Main(string[] args)
{
PointS p1 = new PointS();
PointS p2 = new PointS();
// p1 et p2 dans la pile
// même si on écrit new PointS()
p1.SetXY(2, 3);
// p1 a l'état (2,3)
p2 = p1;
// l'état (2,3) est affecté à p2
Console.WriteLine("p2 = " + p2.ToString());
// p2 = (2,3)
}
Affectation pour les types référence
Une variable de type référence est une variable pour laquelle l'accès se fait via une référence. Tous les objets des
classes sont manipulés ainsi. L'allocation de ces variables est faite dans le tas et leur désallocation est gérée par le
garbage collector de .NET.
PointC p3;
p3 = new PointC();
// p3 est une variable qui peut faire référence à un objet de type
// PointC
// alloue un objet dans le tas et affecte la référence à p3
Dans le cas des variables de type référence, le mot clé new indique que l'on demande à créer une nouvelle
instance. Celle-ci est alors allouée dans le tas (mémoire allouée dynamiquement).
Dans le cas des variables de type référence, on peut voir les références (les variables qui désignent les objets)
comme des sortes de pointeurs sécurisés. Par exemple, une référence qui ne désigne aucun objet vaut la valeur
null. C'est la valeur par défaut d'une référence non initialisée.
PointC p3 = null;
// p3==null signifie "ne désigne aucun objet"
12
C#/.NET
static void Main(string[] args)
{
PointC p3;
// p3 vaut null
PointC p4;
// p4 vaut null
p3 = new PointC();
p4 = new PointC();
// p3 désigne une instance PointC
// p4 désigne une autre instance
p3.SetXY(2, 3);
p4.SetXY(6, 1);
}
ATTENTION! Pour les variables de type référence, l'affectation réalise seulement une copie des références
(comparable à une copie de pointeurs en C++). Dans l'exemple ci-dessous, quand on affecte p3 à p4, puisque
PointC est un type référence, les variables p3 et p4 sont des références. Par conséquent, après l'affectation
p4=p3, p4 fera désormais référence au même objet que p3. Il y aura alors deux références au même objet (celui
d'état (2,3)). L'objet dont l'état est (6,1) n'aura donc plus de référence qui le désigne. Il n'est plus référencé et le
Garbage Collector peut donc désallouer sa mémoire. Mais cette désallocation n'est pas instantanée.
static void Main(string[] args)
{
PointC p3 = new PointC();
PointC p4 = new PointC();
p3.SetXY(2, 3);
p4.SetXY(6, 1);
p4=p3;
// une instance
// une seconde
// l'instance désignée par p3 vaut (2,3)
// celle désignée par p4 vaut (6,1)
// p4 désigne désormais le même objet que p3
// c'est à dire l'instance d'état (2,3)
// l'instance dont l'état est (6,1) n'est plus accessible !
// le Garbage Collector peut libérer sa mémoire
}
3.5. Distinction type valeur/type référence concernant l'égalité (==)
L'opérateur == pour les types valeur
Par défaut (puisque l'opérateur == peut être redéfini), le test d'égalité avec l'opérateur == sur des types valeur
teste l'égalité de valeur des champs. Il y a comparaison de la valeur de tous les champs. L'opérateur renvoie la
valeur true seulement si tous les champs ont les mêmes valeurs deux à deux
L'opérateur == pour les types référence
Par défaut, le test d'égalité avec l'opérateur == teste normalement si deux références désignent le même objet.
Mais puisque l'opérateur == peut être reprogrammé, il peut très bien réalisé un traitement différent. Par exemple,
pour les chaînes de type string, qui est pourtant un type référence, le test avec l'opérateur == indique l'égalité des
chaînes (même longueur et mêmes caractères).
4. L'écriture de classes en C# (types référence)
4.1. Namespaces
Le mot clé namespace permet de définir un espace de nommage. Ceci permet de structurer le nommage des
types (on peut rattacher un type à un espace de noms) et de lever d'éventuels conflits de noms. On peut avoir un
même nom de type dans différents namespaces. Ci-dessous, on définit une classe A dans différents namespaces.
Il s'agit donc de classes différentes.
13
C#/.NET
Les namespaces peuvent être imbriqués. Le nom complet du type A du namespace EspaceA est EspaceA.A.
using System;
namespace EspaceA
{
class A { }
class B { }
}
// namespace avec deux types A et B
namespace EspaceB // namespace avec un espace de nommage imbriqué
{
namespace ClassesImportantes
{
class A { }
}
}
namespace EspaceB.Divers // equivaut à namespace EspaceB{ namespace Divers{... } }
{
class A { }
}
namespace EspaceB.ClassesImportantes // on complète le namespace
{
class B { }
}
namespace ExempleNamespace // namespace de la classe qui contient Main
{
class Program
{
static void Main(string[] args)
{
EspaceA.A p1 = new EspaceA.A();
EspaceB.ClassesImportantes.A p2 = new EspaceB.ClassesImportantes.A();
EspaceB.Divers.A p3 = new EspaceB.Divers.A();
EspaceB.ClassesImportantes.B p4;
}
}
}
Directive using Cette directive permet d'importer un namespace. Celà signifie que s'il n'y a pas d'ambiguité, les
noms de types seront automatiquement préfixés par le namespace.
Par exemple, la classe Console de gestion des entrées sorties console est définie dans le namespace System.
Le nom complet de cette classe est donc System.Console. L'utilisation devrait donc s'écrire
System.Console.WriteLine("Hello!);
Ce qui signifie “appel de la méthode WriteLine du type Console du namespace System”. En important ce
namespace (en début de fichier) avec le mot clé using, on peut simplifier l'écriture
using System;
namespace ConsoleApplication
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello!);
// le type Console est recherché dans
// le namespace System
}
}
}
14
C#/.NET
4.2. Accessibilité
Les membres définis au sein des types peuvent avoir un niveau d'accessibilité parmi
public : membre (donnée ou fonction) accessible par tous
private : membre accessible seulement par une méthode d'un objet de la classe
protected : membre accessible par une méthode d'un objet de la classe ou par une méthode
d'un objet d'une classe dérivée (voir plus tard la dérivation)
C'est grâce à ces modificateurs d'accès que l'on assure
l'encapsulation des données. Tous les membres (attributs,
méthodes ...) doivent être déclarés avec un niveau d'accessibilité.
Par défaut, un membre est privé.
Il y a aussi des modificateurs d'accès spécifiques (non expliqué
dans ce support)
internal : accès autorisé pour l'assemblage courant
protected internal : assemblage courant + sous classes
class MaClasse
{
private int _valeur;
public MaClasse(int v)
{
_valeur=v;
}
public void Affiche()
{
Console.WriteLine(_valeur);
}
}
Ci-dessous un exemple complet avec la définition de la classe MaClasse et son utilisation dans le programme.
Notons qu'il est possible aussi de donner une initialisation des attributs pour les types class. Ce n'est pas
autorisé pour les types struct.
using System;
namespace SyntaxeCSharp
{
class MaClasse
{
private int _valeur =2;
public MaClasse(int v)
{
_valeur = v;
}
// initialiseur de champs
public void Affiche()
{
Console.WriteLine("Objet MaClasse = {0}",
_valeur);
Sortie Console
}
Sortie
Console= 13
Objet
MaClasse
}
Objet
MaClasse
= 13
Mêmes Objets
class Program
Mêmes
Objets
{
Appuyez sur une touche pour
Appuyez sur une touche pour
static void Main(string[] args)
continuer...
{
MaClasse m1 = new MaClasse(13);
continuer...
m1.Affiche();
MaClasse m2 = m1;
// 2ième référence à l'objet
if (m2.Equals(m1)) Console.WriteLine("Mêmes Objets");
}
}
}
4.3. Surcharge des méthodes
Il est possible de surcharger les méthodes, c'est-à-dire définir plusieurs méthodes ayant le même nom mais des
signatures différentes, c'est-à-dire
– dont le nombre de paramètres est diffèrent
15
C#/.NET
–
–
le type des paramètres est différent
les paramètres peuvent aussi avoir même type même nombre mais différer du mode de passage de
paramètre (par valeur ou par référence)
En revanche, le type de retour d'une fonction ne fait pas partie de la signature. On ne peut pas déclarer deux
fonctions qui ne diffèrent que par le type de retour.
class A
{
private int _val;
public int Val(){ return _val; }
public void Val(int v){_val = v;
}
public void Val(out int v){ v = _val;}
}
class Programme
{
static void Main()
{
A refA = new A();
refA.Val(2);
int v;
refA.Val(out v);
Console.WriteLine(refA.Val());
}
}
4.4. Champs constants / en lecture seule
class MaClasse
{
public const int _valC=12;
}
//par défaut statique
Un littéral (constante) doit être utilisé pour initialiser un champ constant.
class MaClasse
{
public readonly int _valRO=12;
}
La valeur d'un attribut readonly peut être fixée par le constructeur. C'est donc une constante dont
l'initialisation de la valeur peut être retardée jusqu'au moment de l'exécution (et non à la compilation).
class MaClasse
{
public readonly int _valRO;
public MaClasse(int val)
{
_valRO=val;
}
}
4.5. Objet courant (référence this)
Dans une méthode, on appelle “Objet Courant”, l'objet sur lequel a été appelée la méthode.
Dans l'exemple ci-dessous, quand on invoque obj1.Compare(obj2), dans la méthode Compare, l'objet
courant est obj1. Dans une méthode, on peut désigner l'objet courant par la référence this.
16
C#/.NET
Notons que par défaut dans une méthode, quand on désigne un attribut (sans préfixer par une référence d'objet),
il s'agit d'un attribut de l'objet courant. Sauf si une variable locale porte le même nom qu'un attribut, ce qui est
déconseillé dans la mesure où c'est source d'erreur.
using System;
namespace ObjCourant
{
class MaClasse
{
private int _val;
public MaClasse(int val)
{
_val = val;
}
public bool Compare(MaClasse obj)
{
// on distingue ici l'attribut de l'objet courant (_val) de
// l'attribut obj._val.
return _val == obj._val; // équivaut à this._val==_obj.val
}
}
class Program
{
static void Main()
{
MaClasse obj1 = new MaClasse(12);
MaClasse obj2 = new MaClasse(14);
Console.WriteLine("obj1 et obj2 ont le même état = {0}", obj1.Compare(obj2));
}
}
}
4.6. Constructeurs (initialisation des objets)
Un constructeur est une méthode qui initialise l'état d'un objet (ne retourne rien) juste après son instanciation. Il a
pour rôle d'éviter qu'un objet ait un état initial indéfini. Comme pour toutes les méthodes, il peut y avoir
surcharge des constructeurs (c'est-à-dire plusieurs constructeurs avec des signatures différentes). De même qu'en
C++, un constructeur a comme nom le nom de la classe.
Un constructeur peut recevoir un ou plusieurs arguments pour initialiser l'objet. Ces arguments sont fournis au
moment de l'instanciation avec le mot clé new.
Remarque : si l'on ne met aucun constructeur dans une classe, il y en a alors un sans paramètre fourni par le
compilateur (ce mécanisme est mis en place par défaut). Ce constructeur fourni par défaut ne fait aucun
traitement mais permet d'instancier la classe. Dès lors qu'au moins un constructeur est spécifié, le constructeur
sans paramètre par défaut n'est plus mis en place.
using System;
namespace SyntaxeCSharp
{
class MaClasse
{
private string _nom;
private int _val;
public MaClasse() // constructeur sans argument
{
_nom = "Nestor";
_val = 12;
}
17
C#/.NET
public MaClasse(int val) // constructeur avec un argument de type int
{
_val = val;
_nom = "Nestor";
}
public MaClasse(int val, string nom){
_val = val;
_nom = nom;
}
} // fin MaClasse
class Program
{
static void Main(string[] args)
{
MaClasse m1 = new MaClasse();
//m1 ->
MaClasse m2 = new MaClasse(23);
//m2 ->
MaClasse m3 = new MaClasse(17, "Paul"); //m3 ->
}
}
("Nestor",12)
("Nestor",23)
("Paul",17)
}
4.6.1. Un constructeur peut appeler un autre constructeur (mot clé this)
On peut utiliser un autre constructeur dans un constructeur. Celà permet de bénéficier d'un traitement
d'initialisation déja fourni par un autre constructeur. La notation utilise également le mot clé this :
public MaClasse(int val):this()
{
}
Un exemple ci-dessous.
using System;
namespace SyntaxeCSharp
{
class MaClasse
{
private string _nom;
private int _val;
public MaClasse()
{
_nom = "Nestor";
_val=12;
}
public MaClasse(int val):this() // appel du constructeur sans paramètre
{
// avant ce bloc
_val=val;
}
/* ce constructeur utilise celui avec 1 paramètre, qui lui même
invoque celui sans paramètre*/
public MaClasse(int val,string nom):this(val)
{
_nom=nom;
}
}
class Program
{
static void Main(string[] args)
{
MaClasse obj=new MaClasse(20);
}
}
//obj._nom=”Nestor” obj._val=20
}
18
C#/.NET
4.7. Membres statiques (données ou méthodes)
Une donnée membre statique (appelée aussi variable de classe) n'existe qu'en un seul exemplaire quel que soit le
nombre d'objets créés (même si aucun objet n'existe).
Une fonction membre statique (appelée aussi méthode de classe) n'accède pas aux attributs des objets. En revanche, une
fonction membre statique peut accéder aux données membres statiques. Une telle fonction peut donc être
invoquée même si aucune instance n'existe. Ce mécanisme remplace les fonctions hors classes du C++. Par
exemple, les fonctions mathématiques sont des fonctions statiques de la classe System.Math.
Console.WriteLine(System.Math.Sin(3.14/2));
using System;
namespace SyntaxeCSharp
{
class MaClasse
{
static private int _nbObjetsCrees = 0;
static public int GetNbObjets()
{
return _nbObjetsCrees;
}
public MaClasse()
{
_nbObjetsCrees++;
}
} // MaClasse
class Program
{
static void Main(string[] args)
{
Console.WriteLine(MaClasse.GetNbObjets());
MaClasse m = new MaClasse();
Console.WriteLine(MaClasse.GetNbObjets());
}
}
}
4.7.1. Constructeur statique
Constructeur appelé une fois avant tout autre constructeur. Permet l'initialisation des variables de classe (champs
statiques.) Il ne faut pas donner de qualificatif de visibilité (public/protected/private) pour ce constructeur.
using System;
namespace SyntaxeCSharp
{
class MaClasse
{
private int _var=12;
private string _str;
static private int _stat;
static MaClasse(){
_stat = 12;
}
public MaClasse(string s){
_str=s;
}
public void Affiche()
{
Console.WriteLine("({0},{1}",_str,_var,_stat);
}
}
19
C#/.NET
class Program
{
static void Main(string[] args)
{
MaClasse m1 = new MaClasse("toto");
MaClasse m2 = new MaClasse("tata");
m1.Affiche();
m2.Affiche();
}
}
}
4.8. Passage de paramètres aux méthodes
4.8.1. Passage par référence (rappel)
Par défaut, les paramètres sont passés par copie (comme en C/C++). Celà signifie que le paramètre formel est une
variable locale à la méthode, initialisée lors de l'appel par le paramètre effectif. Toute manipulation du paramètre
formel n'a pas d'incidence sur le paramètre effectif.
Pour pouvoir modifier les paramètres au sein de la méthode, il faut ajouter le mot clé ref à la fois dans la
définition de la méthode et lors de l'appel
class Programme
{
static public void f(ref int p)
{
p++;
//modifie p le paramètre effectif
}
static void Main()
{
int a=3;
f(ref a);
Console.WriteLine(a);
}
// a vaut 4
}
Il est possible d'avoir des paramètres de sortie seulement.
class Programme
{
static public void f(out int p)
{
p=0;
//modifie p, sa valeur initiale non utilisée
}
static void Main()
{
int a=3;
f(out a);
Console.WriteLine(a);
}
// a vaut 0
}
4.8.2. Méthodes avec un nombre variable de paramètres
L'argument est un tableau contenant les paramètres effectifs.
Le mot clé params indique que la fonction peut avoir un nombre variable de paramètres.
class Programme
{
20
C#/.NET
static void Fonction(params int[] p)
{
Console.WriteLine("{0} paramètres", p.Length);
for(int i=0;i<p.Length;i++)
{
Console.WriteLine("[{0}]",p[i]);
}
}
static void Main(string[] args)
{
Fonction(3);
Fonction(2, 5, 7);
}
}
Sortie Console
Console
1 Sortie
paramètres
1
paramètres
[3]
3 [3]
paramètres
3 paramètres
[2]
[2]
[5]
[5]
[7]
[7]
Appuyez
sur une touche
Appuyez
sur une touche
pour
continuer...
pour continuer...
4.9. Distinction des attributs de type valeur et de type référence
Puisque l'on distingue les objets alloués dans le tas et ceux alloués dans la pile, leur traitement en tant que
membres de classe diffère également.
using System;
namespace SyntaxeCSharp
{
class Programme
{
class A
{
public int _valeur; // type valeur
public A(int valeur) {
_valeur = valeur;
}
public int GetValeur() {
return _valeur;
}
}
class B
{
private double _d;
// type valeur
private A _a;
// type référence
public B()
{
_d = 12.5;
_a = new A(13); // instanciation
}
}
static void Main()
{
A objA = new A(13);
B objB = new B();
}
}
}
4.10. Propriétés
En C#, on peut définir des propriétés. Il s'agit de membres qui donnent l'impression d'être des champs de données
publics, alors que leur accès est contrôlé par des fonctions en lecture et en écriture.
Dans les propriétés, le mot clé value désigne la valeur reçue en écriture (bloc set ).
using System;
namespace SyntaxeCSharp
21
C#/.NET
{
class Programme
{
class MaClasse
{
private int _valeur=2; // membre privé _valeur
public int Valeur
// Propriété Valeur
{
get {
return _valeur;
}
set { // value représente la valeur int reçue en écriture
if(value >=0) _valeur = value;
else _valeur=0;
}
}
}
static void Main()
{
MaClasse m = new MaClasse();
m.Valeur = 13;
// accès en écriture -> set
m.Valeur = -2;
Console.WriteLine(m.Valeur);
// accès en lecture -> get
}
}
}
Les propriétés peuvent être en lecture seule. On ne fournit alors que la partie get de la propriété
using System;
namespace SyntaxeCSharp
{
class MaClasse
{
private int _valeur=2;
public int Valeur // Propriété Valeur en lecture seule
{
get {
return _valeur; }
}
}
class Programme
{
MaClasse m=new MaClasse();
Console.WriteLine(m.Valeur);
}
}
4.11.
Indexeur
Correspond à l'opérateur d'indexation du C++. Ce mécanisme permet de traiter un objet comme un tableau.
Pour utiliser ce mécanisme, il faut définir une méthode this[ ] dont le paramètre est la variable qui sert
d'indice.
Par exemple, l'indexeur ci-dessous utilise un indice entier et retourne un entier. La partie get gère l'accès en
lecture et la partie set l'accès en écriture.
class MaClasse
{
int this[int idx]
{
get{ ...
}
set{ ...
}
}
}
22
C#/.NET
Ci-dessous un exemple où il ya deux indexeurs pour la classe Tableau.
using System;
using System.Collections;
namespace SyntaxeCSharp
{
class Tableau
{
private ArrayList _tab;
public Tableau(){
_tab = new ArrayList(); }
public string this[int idx]
{
get{ return (string)_tab[idx]; }
set
{
if (idx < _tab.Count) _tab[idx] = value;
else
{
while (idx >= _tab.Count) _tab.Add(value);
}
}
}
public int this[string str]
{
get
{
for (int idx = 0; idx < _tab.Count; idx++)
{
if ((string)_tab[idx] == str) return idx;
}
return -1;
}
}
}
class Programme
{
static void Main()
{
Tableau T1 = new Tableau();
T1[2] = "abc";
T1[1] = "def";
Console.WriteLine(T1[0]);
Console.WriteLine("Indice de \"def\" : {0}", T1["def"]);
}
}
}
4.12. Définition des opérateurs en C#
Opérateurs susceptibles d'être redéfinis :
Opérateurs unaires (1 opérande) : + - ! ~ ++ –
Opérateurs binaires (2 opérandes) : + - * / % & | ^ << >>
Règles d'écriture : les opérateurs sont des fonctions membres statiques operator__( ...)
Opérateur unaire : fonction membre statique avec un seul argument, du type de la classe. Doit retourner un objet,
du type de la classe contenant le résultat.
Opérateur binaire : fonction membre statique avec deux arguments, du type de la classe. Doit retourner un objet,
du type de la classe contenant le résultat.
23
C#/.NET
using System;
using System.Collections;
namespace SyntaxeCSharp
{
class MaClasse
{
protected int _valeur=2;
static public MaClasse operator +(MaClasse m1, MaClasse m2)
{
return new MaClasse(m1._valeur + m2._valeur);
}
public override string ToString()
{
return _valeur.ToString();
}
}
class Programme
{
static void Main()
{
MaClasse m1 = new MaClasse(12);
MaClasse m2 = new MaClasse(17);
Console.WriteLine(m1 + m2);
}
}
}
Il est possible de définir un opérateur réalisant la conversion de type.
using System;
namespace SyntaxeCSharp
{
class MaClasse
{
private int _valeur;
public MaClasse(int v) {
_valeur=v;
}
public static explicit operator int(MaClasse m) {
return m._valeur;
}
}
class Programme
{
static void Main(){
MaClasse m1=new MaClasse();
int i;
i=(int)m1; // Conversion explicite de MaClasse -> int
}
}
}
4.13. Conversion Numérique ↔ Chaîne (format/parse)
4.13.1. Formatage (conversion numérique vers string)
Tout objet dérive de object et dispose donc d'une méthode ToString().
int d=12;
string s=d.ToString();
Console.WriteLine(s);
24
C#/.NET
Remarque : il y a aussi pour les types numériques une méthode ToString(string format,
IFormatProvider provider) qui permet de préciser le formattage (cf section sur la classe string)
4.13.2. Parsing (conversion string vers numérique)
Les types numériques (int,float,...) disposent d'une méthode Parse(). Cette méthode génère une exception si le
format de la chaîne est incorrect
static T
Parse(string str)
[ T est le type numérique]
string s;
s=Console.ReadLine(); //lit une chaîne
int var;
try
{
var = int.Parse(s);
var = int.Parse(“13”);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
Une seconde méthode TryParse(), plus rapide, retourne un booléen pour indiquer le succès ou l'échec de la
conversion. Cette méthode ne déclenche pas d'exception.
static bool TryParse(string str, out T var)
string s;
s=Console.ReadLine(); //lit une chaîne
int var;
if(int.TryParse(s,out var)==false)
{
Console.WriteLine(“La chaîne ne respecte pas le format int”);
}
4.14. Les énumérations (types valeur)
Un type créé avec le mot clé enum représente des variables pouvant prendre un nombre fini de valeurs
prédéfinies. Une variable d'un type énumération peut prendre pour valeur une des valeurs définies dans la liste
décrivant l'énumération :
enum Feu{vert,orange,rouge}
...
Feu f;
// f est une variable
f=Feu.vert; //Feu.vert est une valeur
Les valeurs d'une énumération correspondent à des valeurs entières qu'on peut choisir. On peut choisir aussi le
type entier sous-jacent. On peut transtyper une valeur énumération en valeur entière. On peut aussi appliquer
certaines opérateurs :
== (compare), +, -, ++, – , &, |,~
using System;
namespace SyntaxeCSharp
{
enum Color { red, blue, green };
// vaut 0,1,2
enum Access { personal = 1, group = 2, all = 4 };
enum AccessB : byte { personal = 1, group = 2, all = 4 };
// codé sur 8 bits
25
C#/.NET
class Programme
{
static void Main()
{
Color c = Color.blue;
Console.WriteLine("Couleur {0}",(int)c); // Couleur 1
Access a1 = Access.group;
Access a2 = Access.personal;
Console.WriteLine((int)a1);
Console.WriteLine((int)a2);
Access a3 = a1 | a2;
// ou bit à bit sur entier sous-jacent
Console.WriteLine((int)a3);
}
}
}
4.15. Les structures
Le mot-clé struct permet de définir des types valeur ayant des méthodes et des atttributs. L'allocation d'une
variable de type valeur se fait sur la pile alors que les objets de type référence sont alloués dans le tas. L'intérêt
des types struct (type valeur) est de pouvoir gérer les objets plus efficacement (accès mémoire plus rapide et ne
nécessite pas le garbage collector). Certaints types fournis par .NET sont des types struct. En contrepartie,
certaines restrictions s'appliquent aux types créés avec le mot clé struct.
Particularités des types créés avec struct :
Les structures peuvent être converties en type object (comme tout type)
Les structures ne peuvent dériver d'aucune classe ou structure.
Les structures ne peuvent pas servir de base pour la dérivation.
Un type structure peut contenir un/des constructeur(s) mais pas de constructeur sans argument
Les champs ne peuvent pas être initialisés en dehors des constructeurs
Un type structure peut implémenter une interface
Il y a toujours un constructeur par défaut (sans argument) qui fait une initialisation par défaut (0 pour les valeurs
numériques). Ce constructeur reste utilisable même si un autre constructeur a été défini. On peut définir des
propriétés, des indexeurs dans les structures.
Les structures doivent être utilisées principalement pour les objets transportant peu de données membre. Au delà
de quelques octets, le bénéfice obtenu du fait que l'allocation se fait dans la pile est perdu lors des passages de
paramètres, puisqu'un objet de type valeur est dupliqué lorsque l'on fait un passage par valeur.
La portée d'une variable de type valeur va de la déclaration jusqu'à la fin du bloc de la déclaration (puisque les
variables sont alors stockées sur la pile).
using System;
namespace SyntaxeCSharp
{
struct MaStructure
{
private int _valInt;
// pas d'initialisation ici
private double _valDouble;
public MaStructure(int vI, double vD){
_valInt = vI;
_valDouble = vD;
}
public int ValInt
// propriété ValInt
{
get { return _valInt; }
}
26
C#/.NET
public double ValDouble // propriété ValDouble
{
get { return _valDouble; }
set { _valDouble = value; }
}
} // MaStructure
class Programme
{
static void Main()
{
// même si on utilise new → stocké dans la pile
MaStructure s = new MaStructure(12,3.4);
MaStructure s2 = new MaStructure();
Console.WriteLine(s2.ValInt);
} // fin de portée de s1 et s2
}
}
Le framework .NET fournit quelques types valeurs définis comme des structures. Pour la programmation
Windows Forms, on a souvent recours aux structures ci-dessous
System.Drawing.Point : décrit un point du plan
System.Drawing.Rectangle : rectangle
System.Drawing.Size : taille d'une fenêtre
à coordonnées entières int.
Les structures System.Drawing.PointF/System.Drawing.RectangleF/System.Drawing.SizeF
correspondent à la version avec attributs en type flottant.
4.16. Notation UML des classes.
UML (Unified Modelling Language) est une représentation graphique pour les objets. L'UML comprend
différentes représentation graphiques. Parmi les différents diagrammes, le diagramme de classes représente la
structure des classes (attributs et méthodes) ainsi que les relations entre les classes (et donc entre les objets).
Chaque classe est représentée par un rectangle.
La première partie décrit les attributs, la seconde les méthodes. La visibilité est décrite par un symbole
- privé
+ public
# protégé
Le diagramme précédent représente la classe C# ci-dessous. Notons que le détail des méthodes n'est pas indiqué
dans ce diagramme. Seul le nom des méthodes évoque le traitement sous-jacent.
class MaClasse
{
private int _valeur;
public int GetValeur() { ... }
public void SetValeur(int Valeur) { ….
}
}
5. La composition (objet avec des objets membres)
La programmation orientée objet permet de décrire les applications comme des collaborations d'objets. Par
conséquent, dans les diagrammes de classe, certaines relations apparaissent entre les classes. Par exemple,
certaines méthodes d'une classe utilisent les services d'une autre classe. Il n'est pas question de rentrer dans les
détails de la modélisation par UML, mais on peut toutefois s'intéresser à certaines relations importantes comme
27
C#/.NET
la composition et la dérivation (cf. Section suivante)
La composition décrit la situation où un objet est lui même
constitué d'objets d'autres classes. Par exemple un ordinateur
est composé d'une unité centrale et de périphériques. Un livre
est constitué de différents chapitres.
Cette relation particulière est décrite par une relation où un
losange est dessiné côté composite (l'objet qui agrège).
Pour le diagramme ci-contre, la relation entre la classe Réseau et la classe Ordinateur signifie : “un réseau est
constitué de 1 ou plusieurs ordinateurs (1..*) “
Dans le langage objet cible, cette relation de composition est simplement décrite par le fait qu'une classe puisse
avoir un ou plusieurs membres de type classe (ou struct).
class A{
}
class B
{
private A _objet;
public B()
{
_objet = new A();
}
}
class C
{
private A[] _objets;
public C(int nb)
{
_objets = new A[nb];
}
}
Les classes ci-dessus ont les relations suivantes :
un objet de la classe B est constitué d'un objet de la classe A
un objet de la classe C est constitué de 0, 1 ou plusieurs objets de la classe A puisque un objet de
type C stocke éventuellement plusieurs objets de type A.
6. La dérivation
La relation de composition (un objet est composé d'autres objets) n'est pas la seule relation entre les objets. La
dérivation est une seconde relation essentielle de la programmation orientée objet.
La dérivation est un mécanisme fourni par la langage qui permet de spécialiser (la classe décrit des objets plus
spécifiques) une classe existante en lui ajoutant éventuellement de nouveaux attributs / de nouvelles méthodes.
La classe dérivée possède alors intégralement les attributs et les méthodes de la classe de base. On peut donc
considérer un objet de la classe dérivée comme un objet de la classe de base, avec des membres en plus. C'est
une façon de ré-utiliser une classe existante.
Ci-dessous, la classe B dérive de la classe A (c'est indiqué dans l'entête de la classe B). La classe B possède donc
automatiquement les attributs et les méthodes de la classe A, avec en plus des membres spécifiques.
28
C#/.NET
On dit aussi que la classe B hérite des attributs et des méthodes de sa super-classe.
class Program
{
class A
{
private int _valeur;
public A(int val)
{
_valeur = val;
}
public int GetValeur() { return _valeur; }
}
class B : A
{
private bool _etat;
public B(int valeur, bool etat):base(valeur)
{
_etat = etat;
}
public bool GetEtat()
{
return _etat;
}
}
static void Main(string[] args)
{
A objA = new A(12);
Console.WriteLine(objA.GetValeur());
B objB = new B(17, true);
Console.WriteLine(objB.GetValeur());
Console.WriteLine(objB.GetEtat());
}
}
En notation UML, la relation de dérivation est notée avec une flèche partant de la classe dérivée (ou sous-classe)
vers la classe de base.
Un objet de la classe A possède un attribut et une méthode.
Un objet de la classe B possède deux méthodes (GetEtat() et GetValeur()) et deux attributs (_etat et _valeur).
La classe B dérive de la classe A.
B est une sous-classe de la classe A.
A est super-classe ou classe parent de la classe B.
Important : sur le plan de la compatibilité de type, un objet de la classe B peut toujours être considéré comme
un objet de la classe A aussi (puisqu'il possède au moins les attributs et méthodes de la classe de base).
29
C#/.NET
6.1. Syntaxe
On peut définir une classe comme sous-classe d'une classe existante de la façon suivante : la classe Derivee est
sous-classe de la classe MaClasse. Ce qui signifie que tout objet de la classe Derivee dispose des attributs et
méthodes de sa (ses) super-classe(s).
class Derivee : MaClasse
{
protected int _increment;
public Derivee(int val): base(12)
{
_increment=val;
}
}
Tout objet de la classe dérivée dispose des attributs et méthodes hérités de la classe de base (MaClasse) ainsi que
des attributs et méthodes spécifiques décrits dans cette classe (partie incrémentale).
On peut également spécifier qu'une classe ne puisse être dérivée (sealed class).
sealed class MaClasse
{
// cette classe ne peut pas être dérivée
}
6.2. Différence de visibilité protected/private
Un membre privé n'est pas accessible par l'utilisateur de la classe, ni par un objet de la classe dérivée.
Un membre protected n'est pas accessible par l'utilisateur de la classe mais est accessible par un objet de la classe
dérivée. Voir ci-dessous le membre _valeur.
6.3. Initialisateur de constructeur (dans le cas de la dérivation)
Le mot clé base( ) utilisé au niveau du constructeur indique quel constructeur de la classe de base utiliser
pour initialiser la partie héritée d'un objet Derivee.
using System;
namespace SyntaxeCSharp
{
class MaClasse{
protected int _valeur;
public MaClasse(int v){
}// MaClasse
_valeur=v;
}
class Derivee : MaClasse
{
protected double _increment;
public Derivee(double valInc,int valP): base(valP){
_increment=valInc;
}
public void Set(double inc, int val)
{
_valeur = val;
// _valeur accessible car protected
_increment = inc;
}
} // Derivee
class Programme
{
30
C#/.NET
static void Main()
{
MaClasse m1=new MaClasse(2);
Derivee d1=new Derivee(4.3,12);
d1.Set(2.3, 2);
}
}
}
6.4.
Par défaut, les méthodes ne sont pas virtuelles (ligature statique)
De même qu'en C++, les méthodes sont non virtuelles par défaut. Ce qui signifie qu'à partir d'une référence de
type MaClasse, on ne peut appeler que des méthodes de cette classe ou de ses classes de base. Y compris si la
référence désigne en réalité un objet d'une classe dérivée de MaClasse, ce qui est possible avec la compatibilité de
type. On appelle ça une ligature statique des méthodes. La méthode invoquée est alors désignée à la compilation.
Dans le cas de la surdéfinition d'une méthode (non virtuelle) de la classe de base dans la classe dérivée, il faut
tout de même préciser qu'on surdéfinit la méthode (on reconnaît qu'il y a déja une méthode de même nom dans
l'ascendance) en utilisant le mot-clé new.
using System;
namespace SyntaxeCSharp
{
class MaClasse
{
public void Methode(){
Console.WriteLine("MaClasse.Methode");
}
}
class Derivee : MaClasse
{
new public void Methode(){
// masque celle de la classe de base
Console.WriteLine("Derivee.Methode");
}
}
class Programme
{
static void Main()
Sortie Console
Sortie Console
{
MaClasse.Methode
MaClasse.Methode
Derivee.Methode
MaClasse m=new MaClasse();
Derivee.Methode
MaClasse.Methode
Derivee d=new Derivee();
MaClasse.Methode
Appuyez
sur une touche pour
m.Methode();
Appuyez sur une touche pour
continuer...
d.Methode();
continuer...
m=d;
m.Methode();
}
}
}
6.5. Définition des méthodes virtuelles (méthodes à ligature dynamique)
Le mot clé virtual doit être ajouté pour que l'appel d'une méthode bénéficie d'une recherche dynamique de la
“bonne” méthode à l'exécution. En outre, la surdéfinition d'une méthode virtuelle dans la classe dérivée doit être
précédée du mot-clé override (on reconnaît ainsi qu'il y a déja une méthode de même nom dans l'ascendance
et que celle-ci est virtuelle)
using System;
namespace SyntaxeCSharp
{
31
C#/.NET
class MaClasse
{
virtual public void Methode(){
Console.WriteLine("MaClasse.Methode");
}
}
class Derivee : MaClasse
{
override public void Methode(){
Console.WriteLine("Derivee.Methode");
}
}
class Programme
{
static void Main()
{
MaClasse m=new MaClasse();
Derivee d=new Derivee();
m.Methode();
d.Methode();
m=d;
m.Methode();
}
Sortie Console
Sortie Console
MaClasse.Methode
MaClasse.Methode
Derivee.Methode
Derivee.Methode
Derivee.Methode
Derivee.Methode
Appuyez
sur une touche pour
Appuyez sur une touche pour
continuer...
continuer...
}
}
6.6. Une propriété peut être surdéfinie dans la classe dérivée
Qu'elle soit virtuelle ou non, une propriété peut être surdéfinie dans la classe dérivée. Les mêmes règles
s'appliquent que pour les autres méthodes.
using System;
namespace SyntaxeCSharp
{
class Personne
{
protected string _nom;
protected string _prenom;
public Personne(string nom, string prenom){
_nom = nom;
_prenom = prenom;
}
public string Identite{
get { return (_prenom + " " + _nom); }
}
} // Personne
class Enseignant : Personne
{
private int _section;
public Enseignant(string nom, string prenom, int section):base(nom,prenom){
_section = 27;
}
public new string Identite
// redéfinition
{
get{
return String.Format("{0} section {1}",
base.Identite,
_section);
} //fin get
} // fin propriété
} // Enseignant
32
C#/.NET
class Programme
{
static void Main()
{
Enseignant e=new Enseignant("Saubion","Frédéric",27);
Console.WriteLine(e.Identite);
Sortie Console
Personne p = e;
Sortie Frédéric
Console section 27
Console.WriteLine(p.Identite);
Saubion
SaubionFrédéric
Frédéric section 27
}
Saubion
}
Saubionsur
Frédéric
Appuyez
une touche pour
}
Appuyez sur une touche pour
continuer...
continuer...
using System;
namespace SyntaxeCSharp
{
class Personne
{
...
Sortie Console
virtual public string Identite{
Sortie Frédéric
Console section 27
Saubion
get { return (_prenom + " " + _nom); }
Saubion
Frédéricsection
section2727
Saubion Frédéric
}
Saubion
Frédéric
section
Appuyez sur une touche
pour27
} // Personne
Appuyez
sur
une
touche
pour
continuer...
class Enseignant : Personne
continuer...
{
...
override public string Identite
{
get{
return String.Format("{0} section {1}",
base.Identite,
_section);
}
}
}//Enseignant
class Programme
{
static void Main()
{
Enseignant e=new Enseignant("Saubion","Frédéric",27);
Console.WriteLine(e.Identite);
Personne p = e;
Console.WriteLine(p.Identite);
// retrouvée dynamiquement
}
}
}
6.7. Classe object (System.Object)
En .NET, le type object (System.Object) est classe racine de tout type. Tout peut donc être transformé en type
object. Au besoin, pour les types valeur, l'opération de boxing permet de créer un objet compatible avec
object. La classe object dispose des méthodes ci-dessous
bool Equals(object o ) : (virtuelle) indique l'égalité d'objets. Pour les types valeurs, l'égalité de valeur.
Pour les types référence, l'égalité de référence. Attention, ce mécanisme peut être dynamiquement substitué. On
doit donc consulter la documentation du type pour connaître le rôle exact de cette méthode.
static bool Equals(object objA,object objB) : idem en version statique
Type GetType(): retourne un descripteur de type
static bool ReferenceEquals(object objA, object objB): teste si les deux références
désignent le même objet. Cette méthode est non substituable. Elle réalise donc toujours
ce traitement.
33
C#/.NET
virtual string ToString(): retourne le nom du type sous forme de chaîne. Peut être
substitué dans les classes dérivées pour décrire un objet sous forme d'un chaîne.
static void Main(string[] args)
{
int i = 1;
MaClasse m = new MaClasse();
object Obj = i;
Console.WriteLine(Obj.GetType().FullName);
Console.WriteLine(Obj.ToString());
Obj = m;
Console.WriteLine(Obj.GetType().FullName);
Console.WriteLine(Obj.ToString());
Sortie
Sortie
System.Int32
1 System.Int32
1
SyntaxeCSharp.MaClasse
SyntaxeCSharp.MaClasse
SyntaxeCSharp.MaClasse
SyntaxeCSharp.MaClasse
Appuyez
sur une touche pour
Appuyez sur une touche pour
continuer...
continuer...
}
Classe System.Collections.ArrayList : stocke des références à des variables object. Comme object est
l'ancêtre (ou classe de base) de tout type .NET, tout objet (ou variable) peut être considérée comme de type
object. Les conteneurs ArrayList peuvent donc stocker n'importe quel élément, voire même des éléments de
types différents dans le même conteneur.
ArrayList
t=new ArrayList();
t.Add(new MaClasse(12));
t.Add(12L);
t.Add("toto");
foreach(object obj in t)
{
Console.WriteLine(obj.GetType().Name);
Console.WriteLine(obj);
}
6.8. Boxing/unboxing
.NET fait la distinction type valeur/type référence pour que la gestion des variables avec peu de données soit
plus efficace. Pour conserver une uniformité d'utilisation de tous les types, même les variables de type valeur
peuvent être considérées comme de type référence. L'opération qui consiste à créer un objet de type référence à
partir d'une variable de type valeur est appelée Boxing. L'opération inverse permettant de retrouver une
variable à partir d'un type référence est appelée Unboxing.
Boxing : conversion type valeur vers type référence
Unboxing : conversion inverse
Grâce au boxing, on peut considérer une variable int (qui est pourtant un type primitif) en objet compatible avec
les autres classes .NET. Le boxing consiste à créer une variable sur le tas qui stocke la valeur et à obtenir une
référence (compatible object) sur cet objet. L'opération de boxing a donc un coût à l'exécution. Mais ce
mécanisme garantit que toute variable/objet puisse être compatible avec le type ancêtre object.
class Programme
{
static void Main()
{
int var = 2;
object obj = var;
// Boxing
Console.WriteLine(obj.GetType().FullName);
int var2 = (int)obj; //Unboxing
}
}
34
C#/.NET
6.9. Reconnaissance de type (runtime)
Avec .NET (pas seulement C#), les types ont une description stockée dans les modules compilés. Le compilateur
n'est pas le seul a connaître les types utilisés. Ces types sont décrits par des méta-données stockées dans les
modules compilés. On peut donc connaître à l'exécution (runtime) beaucoup d'informations sur les objets
manipulés et leurs types. Tous les types sont représentés par un descripteur de type de la classe System.Type
qui décrit exhaustivement les caractéristiques du type : nom, taille, méthodes et signatures …
Ce mécanisme est dans l'interface de la classe de base object. Tout objet (et même toute variable) dispose
donc d'une méthode GetType qui fournit un descripteur du type de l'objet.
namespace ExempleType
{
class Personne
{
protected string _nom;
protected string _prenom;
public Personne(string nom, string prenom)
{
_nom = nom;
_prenom = prenom;
}
public string Identite
{
get { return (_prenom + " " + _nom); }
}
} // Personne
Sortie
Jacques Durand
ExempleType.Personne
Liste des méthodes de ce
type:
get_Identite
ToString
Equals
GetHashCode
GetType
Appuyez sur une touche pour
continuer...
class Program
{
static void Main(string[] args)
{
Personne p = new Personne("Durand", "Jacques");
Console.WriteLine(p.Identite);
Type t = p.GetType();
Console.WriteLine(t.FullName);
Console.WriteLine("Liste des méthodes de ce type:");
System.Reflection.MethodInfo[] liste = t.GetMethods();
foreach(System.Reflection.MethodInfo minfo in liste)
{
Console.WriteLine(minfo.Name);
}
}
}
}
6.10. Exemple de synthèse sur la dérivation
using System;
namespace SyntaxeCSharp
{
class Base
{
private
int _prive;
protected double _protege;
public Base(int vI, double vD)
{
_prive = vI;
_protege = vD;
}
35
C#/.NET
public void A()
{ Console.WriteLine("Base.A()");}
virtual public void B() { Console.WriteLine("Base.B()");}
public void C()
{ Console.WriteLine("Base.C()");}
} //Base
class Derivee : Base
{
protected string _str;
public Derivee(string vS, int vI, double vD): base(vI, vD)
{
_str = vS;
// _prive n'est pas accessible ici
// _protege est accessible
}
new public void A()
override public void B()
public void D()
} // Derivee
{ Console.WriteLine("Derivee.A()");}
{ Console.WriteLine("Derivee.B()");}
{ Console.WriteLine("Derivee.D()"); }
class Programme
{
static void Main()
{
Derivee d = new Derivee("abc",13, 4.5);
d.A();
d.B();
d.C();
d.D();
Base bD = d;
bD.A();
bD.B();
bD.C();
}
}
Sortie Console
Sortie Console
Derivee.A()
Derivee.A()
Derivee.B()
Derivee.B()
Base.C()
Base.C()
Derivee.D()
Derivee.D()
Base.A()
Base.A()
Derivee.B()
Derivee.B()
Base.C()
Base.C()
Appuyez
sur une touche pour
Appuyez sur une touche pour
continuer...
continuer...
}
6.11. Conversions de type
6.11.1. Notation des casts
Utilise la notation de cast du C/C++. La conversion Dérivé vers Base est toujours possible. On peut par
exemple toujours stocker une référence à n'importe quel objet dans une référence de type object. Dans l'autre
sens, si la conversion est possible, elle nécessite un cast.
class Derivee:Base {
...
}
Base b=new Derivee();
//OK
Derivee d=(Derivee)b; // si b est de type Derivee OK
// sinon, déclenche exception System.InvalidCastException
6.11.2. Opérateur as
Base b;
...
Derivee d=b as Derivee;
// si b ne désigne pas un objet Derivee alors d==null
36
C#/.NET
6.11.3. Opérateur is
Base b;
...
if(b is Derivee) // teste si l'objet désigné par b est de type compatible
{
Derivee d =(Derivee)b; // pas d'exception
...
}
6.11.4. Typeof/Sizeof
Le mot-clé typeof retourne le descripteur de type pour un type donné.
Type t =typeof(int);
L'opérateur sizeof ne s'applique qu'à un type valeur et retourne la taille d'une variable de ce type
Console.WriteLine(sizeof(int));
7. Interfaces et classes abstraites
7.1. Interfaces
Une interface est un ensemble de prototypes de méthodes, de propriétés ou d'indexeurs qui forme un contrat.
Une classe qui décide d'implémenter une interface s'engage à fournir une implémentation de toutes les méthodes
prévues par l'interface.
En contrepartie, dès lors qu'une classe implémente une interface donnée, elle contient nécessairement un
comportement minimal, celui prévu par l'interface.
Une interface décrit uniquement un ensemble de prototypes de méthodes que devront implémenter les classes
qui veulent respecter cette interface :
– pas de qualificatif public/private/protected pour les membres
– pas de données membres
– pas de qualificatif virtual
Ci-dessous une interface avec 2 méthodes et deux classes implémentant cette interface.
using System;
namespace SyntaxeCSharp
{
interface IMonBesoin
{
void A();
void B();
}
class MaClasse:IMonBesoin
{
public void A(){ Console.WriteLine("MaClasse.A()");}
public void B() { Console.WriteLine("MaClasse.B()"); }
} // MaClasse
class AutreClasse : IMonBesoin
{
public void A() { Console.WriteLine("AutreClasse.A()"); }
public void B() { Console.WriteLine("AutreClasse.B()"); }
} // MaClasse
37
C#/.NET
class Programme
{
static void Main()
{
IMonBesoin ib=new MaClasse();
ib.A();
ib.B();
ib=new AutreClasse();
ib.A();
ib.B();
}
}
Sortie Console
Sortie Console
MaClasse.A()
MaClasse.A()
MaClasse.B()
MaClasse.B()
AutreClasse.A()
AutreClasse.A()
AutreClasse.B()
AutreClasse.B()
Appuyez
sur une touche pour
Appuyez sur une touche pour
continuer...
continuer...
}
Un objet implémentant l'interface IMonBesoin peut être référencé au moyen d'une référence de type
IMonBesoin. Cette référence ne permet d'accéder qu'aux méthodes décrites dans l'interface.
7.1.1. Les méthodes d'une interface ne sont pas virtuelles.
using System;
namespace SyntaxeCSharp
{
interface IMonBesoin
{
void A();
void B();
}
class MaClasse:IMonBesoin
{
public void A()
public void B()
}
{ Console.WriteLine("MaClasse.A()");}
{ Console.WriteLine("MaClasse.B()"); }
class DeriveeMaClasse : MaClasse
{
new public void A() { Console.WriteLine("DeriveeMaClasse.A()"); }
new public void B() { Console.WriteLine("DeriveeMaClasse.B()"); }
}
class Programme
{
static void Main()
{
IMonBesoin ib=new DeriveeMaClasse();
ib.A();
ib.B();
}
}
Sortie Console
Sortie Console
MaClasse.A()
MaClasse.A()
MaClasse.B()
MaClasse.B()
Appuyez
sur une touche pour
Appuyez sur une touche pour
continuer...
continuer...
}
Si l'on souhaite que les méthodes soient virtuelles, il faut le préciser dans la classe MaClasse.
using System;
namespace SyntaxeCSharp
{
interface IMonBesoin
{
void A();
void B();
}
38
C#/.NET
class MaClasse:IMonBesoin
{
virtual public void A() { Console.WriteLine("MaClasse.A()");}
virtual public void B() { Console.WriteLine("MaClasse.B()"); }
}
class DeriveeMaClasse : MaClasse
{
override public void A() { Console.WriteLine("DeriveeMaClasse.A()"); }
override public void B() { Console.WriteLine("DeriveeMaClasse.B()"); }
}
class Programme
{
static void Main()
{
IMonBesoin ib=new DeriveeMaClasse();
ib.A();
ib.B();
}
}
Sortie Console
Sortie Console
DeriveeMaClasse.A()
DeriveeMaClasse.A()
DeriveeMaClasse.B()
DeriveeMaClasse.B()
Appuyez
sur une touche pour
Appuyez sur une touche pour
continuer...
continuer...
}
7.1.2. Forcer à utiliser l'abstraction
On peut forcer l'utilisateur à utiliser une classe via son abstraction définie par une interface. Ci-dessous, la
méthode A de l'implémentation ne peut être appelée qu'à partir d'une référence de type IMonBesoin.
using System;
namespace SyntaxeCSharp
{
interface IMonBesoin
{
void A();
void B();
}
class MaClasse:IMonBesoin
{
void IMonBesoin.A()
public void B()
}
{ Console.WriteLine("MaClasse.A()");}
{ Console.WriteLine("MaClasse.B()"); }
class Programme
{
static void Main()
{
MaClasse mc = new MaClasse();
// mc.A(); on ne peut pas utiliser A sur objet MaClasse
mc.B();
IMonBesoin ib = mc;
ib.A();
// on peut utiliser A ici
ib.B();
}
}
}
7.1.3. Interfaces standard .NET
Le framework .NET fournit un ensemble de classes mais aussi un ensemble d'interfaces que certaines classes
doivent implémenter pour bénéficier de mécanismes ad hoc.
Interface System.Collections.IEnumerable : une classe qui implémente cette interface (généralement un
conteneur) fournit un moyen de parcourir ses instances. L'interface IEnumerable standardise le parcours des
conteneurs .NET. En particulier, un objet qui implémente IEnumerable peut être parcouru au moyen de
39
C#/.NET
l'instruction foreach. (cf. Section sur les conteneurs)
Interface System.ICloneable : une classe qui implémente cette interface fournit un moyen de créer une
copie d'un objet de la classe (objet clone).
7.1.4. Tester qu'un objet implémente une interface (opérateurs is et as)
On peut tester, à l'exécution, si un objet donné implémente ou non une interface donnée. On utilise là encore les
opérateurs is et as du langage qui s'appuient sur le CTS pour vérifier la compatibilité de type.
using System;
namespace SyntaxeCSharp
{
interface IMonBesoin
{
void A();
void B();
}
class MaClasse:IMonBesoin
{
public void A()
public void B()
}
{ Console.WriteLine("MaClasse.A()");}
{ Console.WriteLine("MaClasse.B()"); }
class Programme
{
static void Main()
{
object obj = new MaClasse();
if (obj is IMonBesoin)
{
Console.WriteLine("Oui, cet objet implémente l'interface IMonBesoin");
}
IMonBesoin ib = obj as IMonBesoin;
if (ib == null)
{
Console.WriteLine("Non, obj n'implémente pas l'interface IMonBesoin");
}
else
{
Console.WriteLine("Oui, obj implémente l'interface IMonBesoin");
}
}
}
}
7.2. Classes abstraites
Les interfaces ne peuvent décrire qu'une liste de méthodes qu'une classe devra implémenter. On ne peut pas
définir de méthode dans une interface ni même déclarer des données membres. Une classe abstraite correspond
aux classes abstraites du C++. Les méthodes n'ont pas toutes une implémentation. Celle qui n'ont pas
d'implémentation sont marquée abstract. En revanche, certaines méthodes concrètes peuvent être définies
au niveau d'une classe abstraite. C'est ce qui les différencie des interfaces.
Puisqu'une classe abstraite ne définit pas toutes ses méthodes, il n'est pas possible d'instancier une classe
abstraite. En revanche, une classe abstraite sert de base dans le cadre de la dérivation. Toutes les méthodes et
attributs définis dans une classe abstraite sont disponibles dans les classes qui en dérivent.
Ci-dessous, la classe abstraite décrit un système qu'on peut démarrer et arrêter. Tout système a un nom. Ce
traitement élémentaire est décrit directement dans la classe abstraite. Toute classe dérivant de SystemAbstrait
40
C#/.NET
hérite de ce nom et de son accesseur. Toutes les méthodes abstraites sont virtuelles.
using System;
namespace SyntaxeCSharp
{
abstract class SystemeAbstrait
{
private string _nom;
public SystemeAbstrait(string Nom)
{
_nom=Nom;
}
public string GetNom() // méthode concrète
{
return _nom;
}
abstract public void Start(); // méthode abstraite
abstract public void Stop();
abstract public bool IsStarted();
}
class SystemV1 : SystemeAbstrait
{
private bool _on;
public SystemV1(string Nom)
: base(Nom)
{
_on = false;
}
override public void Start()
{
_on = true;
}
override public void Stop()
{
_on = false;
}
// fournit une implémentation
override public bool IsStarted()
{
return _on;
}
}
class Programme
{
static void Main()
{
SystemeAbstrait sys = new SystemV1("Version 1");
Console.WriteLine(sys.GetNom());
sys.Start();
}
}
}
8. Exceptions
De nombreuses fonctions C# sont susceptibles de générer des exceptions, c'est à dire des erreurs. Une exception
est déclenchée lorsqu'un comportement anormal se produit. Lorsqu'une fonction est susceptible de générer une
exception, le programmeur devrait la gérer dans le but d'obtenir des programmes plus résistants aux
erreurs : il faut toujours éviter le "plantage" sauvage d'une application. La gestion d'une exception se fait selon le
schéma suivant :
41
C#/.NET
try{
code susceptible de générer une exception
}
catch (Exception e){
traiter l'exception e
}
instruction suivante
Si la fonction ne génère pas d'exception, on passe alors à instruction suivante, sinon on passe dans le
corps de la clause catch puis à instruction suivante. Notons les points suivants :
• e est un objet de type Exception ou dérivé. On peut être plus précis en utilisant des types tels que
IndexOutOfRangeException, FormatException, SystemException, etc… : il existe plusieurs types
d'exceptions.
En écrivant catch (Exception e), on indique qu'on veut gérer tous les types d'exceptions. Si le code de la clause
try est susceptible de générer plusieurs types d'exceptions, on peut vouloir être plus précis en gérant l'exception
avec plusieurs clauses catch :
try{
code susceptible de générer des exceptions
}
catch (IndexOutOfRangeException e1)
{
traiter l'exception e1
}
catch (FormatException e2)
{
traiter l'exception e2
}
instruction suivante
• On peut ajouter aux clauses try/catch, une clause finally :
try{
code susceptible de générer une exception
}
catch (Exception e)
{
traiter l'exception e
}
finally
{
code toujours exécuté (après try ou catch selon le cas)
}
instruction suivante
Qu'il y ait exception ou pas, le code de la clause finally sera toujours exécuté. Dans la clause catch, on
peut ne pas vouloir utiliser l'objet Exception disponible. Au lieu d'écrire catch (Exception e){..}, on écrit
alors catch(Exception){...} ou plus simplement catch {...}.
La classe Exception a une propriété Message qui est un message détaillant l'erreur qui s'est produite.
La classe Exception a aussi une méthode ToString qui rend une chaîne de caractères indiquant le type de
l'exception ainsi que la valeur de la propriété Message. On pourra ainsi écrire :
catch (Exception ex)
{
Console.WriteLine("L'erreur suivante s'est produite : {0}",
ex.ToString());
42
C#/.NET
...
}//catch
On peut écrire aussi :
catch (Exception ex)
{
Console.WriteLine("L'erreur suivante s'est produite : {0}",ex);
...
}//catch
Le compilateur va attribuer au paramètre {0}, la valeur ex.ToString().
Un exemple où l'on exploite les exceptions liées aux dépassements de valeur
numérique.
class Program
{
static void Main(string[] args)
{
sbyte octet = 100;
L'opération arithmétique a
provoqué un dépassement de
capacité.
100
Appuyez sur une touche
pour continuer...
// bloc où des exceptions sont éventuellement déclenchées
try
{
checked // indique que les dépassements déclenchent des exceptions
{
octet += octet;
}
}
catch (Exception e)
// intercepte les exceptions
// de type Exception (ou compatible)
{
Console.WriteLine(e.Message); // Message lié à l'exception
}
finally // Exécuté après try{} si pas d'exception
// ou après catch{ } si exception
{
Console.WriteLine(octet.ToString()); // affiche la valeur de octet
}
}
}
On peut également préciser le type d'exception, ici il s'agit de OverflowException
class Program
{
static void Main(string[] args)
{
sbyte octet = 100;
// bloc où des exceptions sont éventuellement déclenchées
try
{
checked
{
octet += octet;
}
}
43
C#/.NET
catch (System.OverflowException e)
// intercepte les exceptions de type OverflowException
{
Console.WriteLine(e.Message);
}
}
}
9. Divers
9.1. Types imbriqués
On peut créer un type à l'intérieur d'un autre type. Ci-dessous, la classe B est déclarée à l'intérieur de la classe A.
Les méthodes de la classe B ont donc accès à la partie privée d'un objet de la classe A. Puisque le type B est
public, on peut aussi instancier un objet de la classe A.B (la classe B définie dans A).
using System;
namespace SyntaxeCSharp
{
class A
{
public class B
{
private A _refA;
public B()
public B(A rA){
_refA = null; }
_refA = rA; }
public void ResetA(){
if(_refA!=null) _refA.x = 0;
}
}
private int x = 2;
private B _refB;
public A(){
_refB = new B(this);
public void Reset(){
_refB.ResetA();
}
}
}
class Programme
{
static void Main()
{
A a = new A();
a.Reset();
A.B b = new A.B();
}
}
}
9.2. Dérivation et vérification de type
Le fichier ci-dessous décrit trois classes en relation hiérarchique de dérivation. La classe C dérive de B qui ellemême dérive de A.
Comme l'on peut désigner tout objet par une référence de type object, il est important de savoir vérifier le type
réel d'un objet à l'exécution. On peut utiliser les transtypages, ou les opérateurs is et as.
using System;
namespace SyntaxeCSharp
{
44
C#/.NET
class A
{}
class B : A
{}
class C : B
{}
class Programme
{
static void Main()
{
// type dynamique = type de l'objet désigné par la référence
A a=null;
B b = null;
C c = null;
a
a
a
b
=
=
=
=
new
new
new
new
A();
B();
C();
C();
//
//
//
//
OK
OK
OK
OK
type
type
type
type
dynamique
dynamique
dynamique
dynamique
de
de
de
de
a
a
a
b
=
=
=
=
A
B
C
C
/*****************************************/
// b=a;
// erreur de compilation (utiliser cast ou is/as)
/*****************************************/
/*************************************************/
// opérateur is : vérification dynamique de type
/*************************************************/
a = new C();
if (a is B)
{
/* (a is B) booléen qui indique si le type
dynamique de a est B ou d'une sous-classe de B */
// ici (a is B)==true puisque a est de type dynamique C
}
if (a is C) { // (a is C) == true
}
a= null;
if (a is C){ // (a is null)==false
}
/*************************************************/
// operateur as : conversion dynamique de type
/*************************************************/
a = new B();
b = a as B;
/// equivaut à if(a is B) { b = (B)a;} else {b = null;}
if(b!=null){
// a désignait un objet compatible avec B, b est sa référence
}
c = a as C; // (a as C)==null puisque a n'est pas de type dynamique C
if(c==null){
Console.WriteLine("a n'est pas de type C");
// l'objet désigné par a n'est pas de type compatible avec C
}
/*************************************************/
// cast et exception
// les cast impossibles déclenchent des exceptions
45
C#/.NET
/*************************************************/
a = new B();
try
{
b = (B)a;
c = (C)a;
// cast OK car le type dynamique de a est B
// ce cast déclenche une exception interceptée ci-dessous
}
catch(Exception e)
{
Console.WriteLine(e.ToString());
}
}
}
}
9.3. Accès aux membres non virtuels hérités
Après dérivation, un objet peut disposer de plusieurs méthodes non virtuelles de même nom. Dans l'exemple cidessous, un objet de la classe C dispose de 3 méthodes f(). Par défaut, une référence de type A ne permet
d'atteindre que la méthode de la classe A (puisque f n'est pas virtuelle). Inversement, avec une référence de type
C, on appelle la méthode de la classe C. Pour pouvoir utiliser les autres méthodes héritées, il faut transtyper.
using System;
namespace SyntaxeCSharp
{
class A
{
public void f() { Console.WriteLine("A.f()"); }
}
class B : A
{
new public void f() { Console.WriteLine("B.f()"); }
}
class C : B
{
new public void f() { Console.WriteLine("C.f()"); }
}
Sortie Console
Sortie Console
C.f()
C.f()
B.f()
B.f()
B.f()
B.f()
A.f()
A.f() sur une touche pour
Appuyez
Appuyez sur une touche pour
continuer...
continuer...
class Programme
{
static void Main()
{
C c = new C();
c.f();
B b = c;
b.f();
((B)c).f(); // appelle la méthode de B en raison du cast
((A)c).f(); // appelle la méthode de A en raison du cast
}
}
}
9.4. Désactiver le polymorphisme dans une hiérarchie de classes
En C++, dès lors qu'une méthode est virtuelle, toutes les surdéfinitions de cette méthode dans les sous-classes
disposent nécessairement du mécanisme de recherche dynamique à l'exécution. Ce mécanisme ne peut pas être
contraint ou désactivé. En C#, on peut contrôler les méthodes pour lesquelles le mécanisme de recherche
46
C#/.NET
dynamique à l'exécution s'applique.
Dans l'exemple ci-dessous, la méthode B.g() n'est plus virtuelle. Le mot clé new indique que le mécanisme ne
doit pas s'appliquer pour les appels de cette méthode sur un objet B. Alors que la méthode f() est toujours
virtuelle. On peut même regrouper les mots clé new et virtual. Voir le comportement de l'appel de la méthode f()
selon qu'il s'applique à un objet B, C ou D et selon que la référence est de type A, B ou C.
using System;
namespace SyntaxeCSharp
{
class A
{
virtual public void f() { Console.WriteLine("A.f()"); }
virtual public void g() { Console.WriteLine("A.g()"); }
}
class B : A
{
override public void f() { Console.WriteLine("B.f()"); }
new public void g() { Console.WriteLine("B.g()"); }
}
class C : B
{
new virtual public void f() { Console.WriteLine("C.f()"); }
new virtual public void g() { Console.WriteLine("C.g()"); }
}
class D : C
{
override public void f() { Console.WriteLine("D.f()"); }
override public void g() { Console.WriteLine("D.g()"); }
}
class Programme
{
static void Main()
{
A a1 = new B();
A a2 = new C();
A a3 = new D();
B b1 = new C();
B b2 = new D();
C c1 = new D();
a1.f();
a2.f();
a3.f();
b1.f();
b2.f();
c1.f();
Console.WriteLine("-------");
a1.g();
a2.g();
a3.g();
b1.g();
b2.g();
c1.g();
}
}
}
Sortie Console
Sortie Console
B.f()
B.f()
B.f()
B.f()
B.f()
B.f()
B.f()
B.f()
B.f()
B.f()
D.f()
D.f()
------------A.g()
A.g()
A.g()
A.g()
A.g()
A.g()
B.g()
B.g()
B.g()
B.g()
D.g()
D.g() sur une
Appuyez
Appuyez
sur une
touche
pour
touche
pour
continuer...
continuer...
10. Délégation (delegate) et Evénements (event)
Cet aspect de .NET (que l'on retrouve donc aussi en VB.NET) apparaît comme une nouveauté par rapport à
Java. Le framework fournit un mécanisme pour pouvoir déléguer des appels de fonctions à un objet. Ce
mécanisme présente des similitudes avec la notion de pointeur de fonction en C/C++.
47
C#/.NET
10.1. Délégation
Le mot-clé delegate permet de créer des classes particulières appelées classes de délégation. Dans l'exemple cidessous, la classe de délégation est Deleg. Une instance de cette classe est appelée objet délégué ou délégué.
Une classe de délégation dérive implicitement de la classe System.MulticastDelegate. Un objet délégué doit
assurer des appels à des méthodes qu'on lui a indiquées.
Lors de la définition de la classe de délégation, on mentionne également la signature des méthodes prises en
charge par la délégation. Une instance de Deleg permettra d'appeler des méthodes acceptant un paramètre
double et retournant un int.
Ci-dessous, Round désigne une instance de la classe Deleg. On peut ensuite utiliser l'objet Round comme s'il
s'agissait d'une fonction acceptant un paramètre double et retournant un entier. A l'instanciation, on précise
quelle méthode utiliser pour la délégation. Ensuite, on peut changer dynamiquement la méthode utilisée par
l'objet délégué. Les opérateurs -= et += agissent sur l'objet délégué pour retirer ou ajouter une méthode.
using System;
namespace SyntaxeCSharp
{
class Program
{
delegate int Deleg(double x);
static int Floor(double x) {
static int Ceil(double
x)
return (int)x;
}
{ return (int) Math.Ceiling((double) x);}
static void Main(string[] args)
{
int val;
Deleg Round=new Deleg(Floor);
val = Round(3.2);
// utilise Floor
Console.WriteLine(val);
Round -= new Deleg(Floor); // retire Floor
Round += new Deleg(Ceil); // utilise Ceil
val = Round(3.2);
Console.WriteLine(val);
}
Sortie Console
3 Sortie Console
43
4
Appuyez
sur une touche
Appuyez
sur une touche
pour
continuer...
pour continuer...
}
}
Simplification d'écriture avec C# 2
Avec la deuxième version de C#, le compilateur peut inférer certains types ou signatures. La définirion de la
classe Deleg est identique. Seule la création des objets délégués devient un peu plus simple.
using System;
namespace SyntaxeCSharp
{
class Program
{
delegate int Deleg(double x);
static int Floor(double x) { return (int)x; }
static int Ceil(double x) { return (int)Math.Ceiling((double)x); }
static void Main(string[] args)
{
int val;
Deleg Round = Floor;
val = Round(3.2);
// utilise Floor
48
C#/.NET
Console.WriteLine(val);
Round -= Floor;
// retire Floor
Round += Ceil;
// utilise Ceil
val = Round(3.2);
Console.WriteLine(val);
}
}
}
10.2. Plusieurs appels pour un délégué
Un objet délégué peut prendre en charge l'appel d'une méthode mais aussi d'une liste de méthodes. Dans
l'exemple ci-dessous, une instance de la classe CallBack permet d'appeler plusieurs fonctions.
using System;
namespace SyntaxeCSharp
{
class Program
{
delegate void CallBack();
public static void CB1()
{
Console.WriteLine("CB1()");
}
public static void CB2()
{
Console.WriteLine("CB2()");
}
static void Main(string[] args)
{
CallBack fonctionCB = CB1;
Console.WriteLine("----------------------");
fonctionCB(); // appel de CB1()
Console.WriteLine("----------------------");
fonctionCB += CB2; // ajout de CB2()
fonctionCB += CB1; // ajout de CB1()
fonctionCB(); // appel de CB1()->CB2()->CB1()
Console.WriteLine("----------------------");
fonctionCB -= CB2; // retrait de CB2()
fonctionCB(); // appel de CB1()->CB1()
Console.WriteLine("----------------------");
}
}
}
10.3. Appels d'une méthode d'une classe par un délégué
Les exemples précédents de délégation utilisaient des méthodes statiques. Un délégué peut aussi invoquer une
méthode d'une classe mais il faut alors préciser sur quel objet elle devra être appelée. Ci-dessous, l'objet
fonctionCB peut déclencher l'appel de a.CB2().
using System;
namespace SyntaxeCSharp
{
class A
{
private int _val;
49
C#/.NET
public A(int v) { _val = v; }
public void CB2()
{
Console.WriteLine("A.CB2() avec A._val = {0}",_val);
}
}
class Program
{
delegate void CallBack();
public static void CB1()
{
Console.WriteLine("CB1()");
}
static void Main(string[] args)
{
A a = new A(13);
// instance de A
CallBack fonctionCB = a.CB2;
Console.WriteLine("----------------------");
fonctionCB();
// a.CB2()
Console.WriteLine("----------------------");
fonctionCB += CB1;
Sortie Console
Sortie Console
------------------------------------------A.CB2()
avec A._val = 13
A.CB2() avec A._val = 13
------------------------------------------A.CB2()
avec A._val = 13
A.CB2() avec A._val = 13
CB1()
CB1()
------------------------------------------Appuyez
sur une touche
Appuyez
sur une touche
pour
continuer...
pour continuer...
fonctionCB();
// a.CB2()->CB1()
Console.WriteLine("----------------------");
}
}
}
10.4. Evénéments
On peut voir les événements comme une forme particulière de délégation. La similitude repose sur le fait qu'un
événement peut déclencher l'appel d'une ou plusieurs méthodes (comme pour une délégation). En revanche,
selon que l'on est dans la classe propriétaire de l'événement ou non, on n'a pas les même droits d'utilisation.
Seule la classe propriétaire de l'événement peut déclencher des appels. Par contre, n'importe quel utilisateur peut
abonner/désabonner une méthode à l'événement, c'est-à-dire ajouter ou retirer une méthode de la liste des
métodes invoquées par l'événement.
Cet aspect du langage permet de simplifier la mise en oeuvre du design pattern Observer. En C#, le sujet est une
classe contenant un événement. Les observateurs sont toutes les classes abonnant une de leurs méthodes à
l'événement du sujet.
Dans l'exemple ci-dessous, la classe Emetteur a un événement appelé EmetteurEvent. Cet événement est
déclenchable par tout objet de la classe Emetteur. Lorsque l'événement est déclenché, les abonnés à cet
événement sont notifiés.
A noter, un événement est toujours déclaré à partir d'un type de délégation. Le type de délégation décrit la
signature des méthodes pouvant s'abonner à l'événement.
L'événement ci-dessous ne déclenche que des méthodes sans paramètre ni retour.
class Emetteur
{
public delegate void CallBack();
// délégation
public event CallBack EmetteurEvent; // événement
public void DeclencheEvenement()
{
50
C#/.NET
// déclenche un événement = appel d'une ou plusieurs méthodes abonnées
EmetteurEvent();
}
}
class Program
{
public static void CB1(){
Console.WriteLine("Call Back 1");
}
public static void CB2(){
Console.WriteLine("Call Back 2");
}
static void Main(string[] args)
{
Emetteur e = new Emetteur();
Sortie Console
Sortie
Call
BackConsole
1
Call
Back2 1
Call Back
Call Back
Appuyez
sur 2une touche
Appuyez
sur une touche
pour continuer...
pour continuer...
e.EmetteurEvent += CB1; // abonne CB1 à l'événement e.EmetteurEvent
e.EmetteurEvent += CB2; // abonne CB2 à l'événement e.EmetteurEvent
e.DeclencheEvenement();
// e.EmetteurEvent(); pas possible hors classe Emetteur
}
}
10.5. Exploitation standard des événements dans .NET
L'utilisation des événements, notamment dans les classes System.Windows.Forms, suit toujours un peu la méme
architecture. Les événements sont abondamment utilisés dans les application WinForms, puisque tous les
événements du système (souris, clavier, …) sont vus comme des événements dans les classes. Un gestionnaire
d'événement consiste donc à abonner une méthode de notre cru à un événement donné.
Dans ce schéma là, les événements ont généralement une signature (défini par une délégation) assez standard.
Les événements appellent des méthodes ayant le plus souvent deux paramètres. Le premier est une référence à
l'objet qui déclecnhe l'événement (le sujet ci-après). Le second est une référence à un objet censé transporter des
données associées à l'événement. Pour un événement click souris, les données transmises sont les coordonnées
du click. Le second paramètre est une référence de type System.EventArgs (ou dérivé).
Si l'on reprend ce schéma, pour une application console où l'on souhaite que le sujet puisse notifier un
observateur, on obtient l'application ci-après.
La classe Sujet est propriétaire de l'événement.
La classe Donnees décrit les données transmises par l'événement (dérive de System.EventArgs).
System.EventHandler est une délégation standard fournie par .NET qui décrit la signature d'une méthode ne
retournant rien, ayant un paramètre object et un second paramètre System.EventArgs.
using System;
namespace SyntaxeCSharp
{
class Donnees : EventArgs
{
private string _message;
public Donnees(string msg){_message=msg;}
public string Msg(){ return _message;
}
}
class Sujet
{
public event EventHandler EvtSup20;
private int _var;
public Sujet(int v){ _var = v; }
public int Var // propriété
{
51
C#/.NET
get
{
return _var;
}
set
{
_var = value;
if (_var > 20)
{
EvtSup20(this, new Donnees("Message attaché transmis par sujet"));
}
}
}
}
class Program
{
public static void CB(object o, EventArgs args)
{
Console.WriteLine("Fonction Call Back ");
Sujet s= o as Sujet;
if (s != null)
{
Console.WriteLine("L'état de l'annonceur = {0}",s.Var);
Donnees d = args as Donnees;
if(d !=null)
{
Console.WriteLine("Message du sujet = {0}",d.Msg());
}
}
Sortie Console
}
Sortie Console
Fonction
Call Back
Fonction
Call Back = 24
L'état
de l'annonceur
L'état
de
l'annonceur
= 24attaché
Message du sujet
= Message
Message
du
sujet
=
Message
attaché
transmis par sujet
transmis
par
sujet
public static void Main()
Appuyez sur une touche pour continuer...
{
Appuyez sur une touche pour continuer...
Sujet leSujet = new Sujet(12);
leSujet.EvtSup20 += CB;
leSujet.Var = 17; //pas d'événement
leSujet.Var = 24; //événement -> les abonnés sont informés -> CB appelé
}
}
}
11. Les classes conteneurs
Le framework .NET fournit des classes pour stocker des données. Nous décrivons sommairement quelques
classes d'usage fréquent.
11.1. Classe string
11.1.1. Les membres de la classe string
Le type string du C# est un alias du type System.String de .NET qui permet de gérer des chaînes de
caractère. Il s'agit d'un type référence mais dont l'usage ressemble à celui des types valeurs. Pour le type
string, l'affectation fait en réalité une copie des valeurs (et non une copie de références). L'autre
caractéristique est que les objets de cette classe sont immutables. Celà signifie que les objets gardent la même
valeur du début à la fin de leur vie. Toutes les opérations visant à changer la valeur de l'objet retourneront en
réalité un nouvel objet.
Quelques membres de la classe System.String
52
C#/.NET
Constructeurs
string(Char[] tabC)
Initialise une nouvelle instance de la classe String à la valeur
indiquée par un tableau de caractères Unicode.
string(Char, Int32 repete)
Initialise une nouvelle instance de la classe String à
la valeur
indiquée par un caractère Unicode et répété un certain
nombre de fois.
string(Char[], Int32 index, Int32 count)
Initialise une nouvelle instance de la classe
String à la valeur indiquée par un tableau de caractères Unicode, un point de départ pour
le caractère dans ce tableau et une longueur.
class Program
{
static void Main(string[] args)
{
string s1 = "abc";
string s2 = new string('*', 7);
string s3 = new string(new char[] { 'e', 'f', 'g' });
Console.WriteLine(s1);
Console.WriteLine(s2);
Console.WriteLine(s3);
// abc
// *******
// efg
}
}
Champs publics
Nom
Empty
Description
Représente la chaîne vide. Ce champ est en lecture seule.
Propriétés publiques
Nom
Description
indexeur [] Obtient le caractère à une position spécifiée dans cette instance.
Length
Obtient le nombre de caractères dans cette instance.
Méthodes publiques
public bool EndsWith(string value)
public bool StartsWith(string value)
rend vrai si la chaîne se termine par value
rend vrai si la chaîne commence par value
public virtual bool Equals(object obj)
rend vrai si la chaînes est égale à obj –
équivalent chaîne==obj
public int IndexOf(string value, int startIndex) rend la première position dans la chaîne
de la chaîne value - la recherche commence à partir du
caractère n° startIndex
public int IndexOf(char value, int startIndex) idem mais pour le caractère value
public string Insert(int startIndex, string value) insère la chaîne value dans chaîne en
position startIndex
public string Remove(int startIndex,int count) supprime count caractères à partir de
startIndex
public static string Join(string separator,string[] value) méthode de classe - rend une
chaîne de caractères, résultat de la concaténation des
valeurs du tableau value avec le séparateur separator
public int LastIndexOf(string value, int startIndex, int count) idem indexOf mais rend la
dernière position au lieu de la première
53
C#/.NET
public int LastIndexOf(char value, int startIndex,int count)
public string Replace(char oldChar, char newChar) rend une chaîne copie de la chaîne
courante où le caractère oldChar a été remplacé par le caractère
newChar
public string[] Split(char[] separator) la chaîne est vue comme une suite de champs
séparés par les caractères présents dans le tableau separator. Le
résultat est le tableau de ces champs
public string Substring(int startIndex, int length) sous-chaîne de la chaîne courante
commençant à la position startIndex et ayant length caractères
public string ToLower() rend la chaîne courante en minuscules
public string ToUpper() rend la chaîne courante en majuscules
public string Trim(char[] jeu ) supprime toutes les occurences d'un jeu de caractères
static void Main(string[] args)
{
string s1 = "chaîne de caractères";
Console.WriteLine(s1.Contains("car"));
Console.WriteLine(s1.StartsWith("ch"));
Console.WriteLine(s1.IndexOf('c'));
Console.WriteLine(s1.LastIndexOf('c'));
//
//
//
//
True
True
0
14
Console.WriteLine(s1.Substring(4, 7));
// ne de c
}
11.1.2. Le formattage de types numériques
La représentation des valeurs numériques par des chaînes de caractères est décrite dans l'interface
IFormattable, c'est à dire une classe disposant d'une méthode ToString() avec deux paramètres
interface IFormattable
{
string ToString(string format,IFormatProvider fprovider)
}
format : chaîne fournissant des instructions de formattage constituée d'une lettre avec un digit optionnel
fprovider : objet permettant de réaliser les instructions de formattage selon une
culture donnée
Le programme ci-dessous affiche un prix avec au plus deux chiffres après la virgule.
Dans la culture en-GB (english Great Britain) , le prix est affiché en livres. Le prix est
affiché en dollars dans la culture en-US (english US).
static void Main(string[] args)
{
double prix = 3.5;
CultureInfo culture = CultureInfo.GetCultureInfo("en-GB");
Console.WriteLine(prix.ToString("C2", culture));
culture = CultureInfo.GetCultureInfo("en-US");
Console.WriteLine(prix.ToString("C2", culture));
}
£3.50
$3.50
Appuyez sur une
touche pour
continuer...
54
C#/.NET
Chaîne de formattage des types numériques (Lettre + digit)
G ou g = général F = fixed point N = fixed point avec séparateur de groupe
E = notation exp P = pourcentage X ou x =hexadécimal
class Program
{
static void Main(string[] args)
{
double val1 = 1.23456;
double val2 = 12340;
double val3 = 0.0001234;
double val4 = 1203.01234;
double val5 = 0.1234;
CultureInfo fr = CultureInfo.GetCultureInfo("fr-FR");
Console.WriteLine("----------------");
// G ou g : Général
Console.WriteLine("G");
Console.WriteLine(val1.ToString("G", fr));
Console.WriteLine(val2.ToString("G", fr));
Console.WriteLine(val3.ToString("G", fr));
Console.WriteLine("----------------");
// "G3" = limité à 3 digits au total (passe en notation
// exp au besoin)
Console.WriteLine(val1.ToString("G3", fr));
Console.WriteLine(val2.ToString("G3", fr));
Console.WriteLine("----------------");
// F : Fixed point
// F2 arrondit à 2 décimales
Console.WriteLine("F");
Console.WriteLine(val1.ToString("F2", fr));
Console.WriteLine(val3.ToString("F2", fr));
Console.WriteLine("----------------");
// N : Fixed point + séparateur de groupe
// N2 limite à deux décimales
Console.WriteLine("N");
Console.WriteLine(val1.ToString("N2", fr));
Console.WriteLine(val2.ToString("N2", fr));
Console.WriteLine(val4.ToString("N2", fr));
---------------G
1,23456
12340
0,0001234
---------------1,23
1,23E+04
---------------F
1,23
0,00
---------------N
1,23
12 340,00
1 203,01
---------------E
1,203012E+003
1,2030E+003
---------------P
12,34 %
12,3 %
---------------X
1E
Appuyez sur une
touche pour
continuer...
Console.WriteLine("----------------");
Console.WriteLine("E");
// E : notation exp (6 digits par défaut après virgule)
// E4 : 4 digits après la virgule
Console.WriteLine(val4.ToString("E", fr));
Console.WriteLine(val4.ToString("E4", fr));
Console.WriteLine("----------------");
// P : pourcentage
Console.WriteLine("P");
Console.WriteLine(val5.ToString("P", fr));
Console.WriteLine(val5.ToString("P1", fr));
Console.WriteLine("----------------");
// X ou x :hexadécimal
Console.WriteLine("X");
Console.WriteLine(30.ToString("X"));
}
}
55
C#/.NET
11.2. Les tableaux
En C#, les tableaux sont des objets de type référence, compatibles avec le type de base Array. Le type Array est
une classe abstraite que seul le compilateur peut dériver et implémenter. Pour construire des objets tableaux, on
utilise les constructions décrites ci-dessous.
Il faut retenir que les tableaux générés ainsi ne sont pas redimensionnables. Dès lors qu'il est nécessaire de
changer de dimension, les méthodes utilisées re-créent de nouveaux tableaux.
11.2.1. Tableau unidimensionnel
Déclaration des tableaux (à une dimension):
int[] tab;
// référence à un tableau (ausun objet n'est créé)
Instanciation : car un tableau est aussi vu comme un objet implémentant System.Array
int[] t1,t2;
int[] t3={2,6};
t1=new int[3];
t2=new int[]{1,2,3};
//
//
//
//
déclaration de références
déclaration + instanciation + initialisation
instanciation
instanciation + initialisation
Les tableaux sont compatibles avec le type System.Array (qui est classe de base)
int[] tab=null;
tab = new int[3];
Console.WriteLine(tab.GetType());
System.Array t=tab; // t est une référence à tab
t.SetValue(3, 0);
// modifie de fait tab[0]
11.2.2. Parcours d'un tableau
Length = propriété fournissant le nombre d'éléments d'un tableau
(toutes dimensions confondues)
int[] tab = new int[3];
for (int i = 0; i < tab.Length; i++)
{
tab[i]=i+1;
Console.WriteLine(tab[i]);
}
foreach : autre façon de parcourir (accès en lecture seule)
int[] tab = new int[3];
foreach (int val in tab)
{
Console.WriteLine(val);
}
11.2.3. Tableau à plusieurs dimensions
int[, ,] cube = new int[3, 4, 3];
cube[0,0,0] = 1;
// tableau à 3 dimensions
// écriture dans ce tableau
La propriété cube.Length fournit alors le nombre d'éléments = 36
int[, ,] cube = new int[3, 4, 3];
cube[0,0,0] = 1;
Console.WriteLine(cube.Length);
Console.WriteLine(cube.GetLength(0));
Console.WriteLine(cube.Rank);
// 36
// taille dans la première dimension
// nombre de dimensions = 3
56
C#/.NET
11.2.4. Tableaux déchiquetés (jagged arrays)
int[][] monTab;
monTab = new int[4][];
// monTab est un tableau
// pouvant stocker 4 tableaux d'int
monTab[0] = new int[4];
monTab[1] = new int[2];
monTab[2] = new int[8];
monTab[3] = new int[3];
Console.WriteLine(monTab[0][0]);
11.2.5. Tableaux hétérogènes
Puisque tout peut être vu comme étant de type object :
object[]
tab[0] =
tab[1] =
tab[2] =
tab = new object[3];
12;
"toto";
12.4F;
11.2.6. Copie de tableaux
Première version, utilise la méthode Array.CopyTo()
int[] t1 = {1,2,3};
int[] t2 = new int[t1.Length];
t1.CopyTo(t2,0);
Seconde version, utilise la méthode Clone()qui pour les objets clonables (implémentant ICloneable),
renvoit un objet copie
int[] t1 = {1,2,3};
int[] t2;
t2= (int[]) t1.Clone();
// t1.Clone() est de type object, d'où cast
11.2.7. Membres de la classe Array
Array est une classe abstraite implémentant certaines interfaces
namespace System
{
public abstract class Array : ICloneable, IList,
ICollection,IEnumerable
{ }
}
Propriétés
public int Length {get;}
nombre total d'éléments du tableau, quel que soit
son nombre de dimensions
public long LongLength {get;}
idem mais sur 64 bits
public int Rank {get;}
nombre total de dimensions du tableau
Méthodes
public static int BinarySearch<T>(T[] tableau,T value) rend la position de value dans
un tableau trié unidimensionnel
public static int BinarySearch<T>(T[] tableau,int index, int length, T value) idem mais
cherche dans tableau trié à partir de la
position index et sur length éléments
57
C#/.NET
public static void Clear(Array tableau, int index, int length) met les length éléments de
tableau commençant au n° index à 0 si numériques, false si booléens,
null si références
public static void Copy(Array source, Array destination, int length) copie length
éléments de source dans destination
public int GetLength(int i) nombre d'éléments de la dimension n° i du tableau
public int GetLowerBound(int i) indice du 1er élément de la dimension n° i
public int GetUpperBound(int i) indice du dernier élément de la dimension n° i
public static int IndexOf<T>(T[] tableau, T valeur) rend la position de valeur dans
tableau ou -1 si valeur n'est pas trouvée.
public static void Resize<T>(ref T[] tableau, int n) redimensionne tableau à n éléments.
Les éléments déjà présents sont conservés.
public static void Sort<T>(T[] tableau, IComparer<T> comparateur) trie tableau selon un
ordre défini par comparateur.
11.3. Les conteneurs génériques.
Le C# permet la définition de classes paramétrées en type. Celà signifie que certains types peuvent être choisis au
moment de l'instanciation. Du fait que certains types sont paramétrables, on parle de classe générique.
Sans rentrer dans les détails, on peut fournir un exemple simple de classe générique. On représente des paires de
valeurs. Le type des valeurs de la paire est un paramètre de type de la classe.
namespace ConsoleGenerique
{
class Paire<T> // classe générique : T est yb type paramétrable
{
private T _premier; // les deux attributs sont du même type
private T _second;
public Paire(T prem, T sec)
{
_premier = prem;
_second = sec;
}
// constructeur
public override string ToString()
{
return "(" + _premier.ToString() + "," + _second.ToString() + ")";
}
}
class Program
{
static void Main(string[] args)
{
// paire d'entiers
Paire<int> p1= new Paire<int>(7,2);
Console.WriteLine(p1.ToString());
Sortie console
(7,2)
(premier,second)
((1,2,2,3),
(3,5,4,8))
// paire de chaînes
Paire<string> p2 = new Paire<string>("premier", "second");
Console.WriteLine(p2.ToString());
// paire de paires de double (puisque Paire<double> est aussi un type)
Paire<Paire<double>> p3;
p3 = new Paire<Paire<double>>(new Paire<double>(1.2, 2.3),
new Paire<double>(3.5, 4.8));
Console.WriteLine(p3.ToString());
}
58
C#/.NET
}
}
11.3.1. Quelques interfaces de conteneurs : IEnumerable, ICollection, Ilist, IDictionary
Les classes conteneurs sont toutes les classes fournies par le framework .NET qui permettent de stocker des
données. Le premier type de conteneur utilisé est le tableau (type dérivé de Array). D'autres classes génériques
permettent de stocker des données. Les conteneurs se distinguent par le type de service qu'ils rendent.
Interface IEnumerable<T>
Le comportement minimum d'une structure de donnée est d'implémenter IEnumerable. C'est à dire que la
structure de donnée peut fournir un énumérateur permettant de parcourir les données.
Dès lors qu'un type implémente IEnumerable, on peut utiliser l'instruction foreach pour parcourir ses
éléments.
public interface IEnumerable
{
// Retourne: Objet System.Collections.IEnumerator pouvant
// être utilisé pour itérer au sein de la collection.
IEnumerator GetEnumerator();
}
public interface IEnumerator
{
object Current { get; }
// fournit élément courant.
// Avance l'énumérateur à l'élément suivant de la collection.
// Retourne: true si l'énumérateur a pu avancer
bool MoveNext();
//
remet l'énumérateur à sa position initiale (avant premier élément)
void Reset();
}
Interface ICollection<T>
Plus riche que l'interface IEnumerable, l'interface ICollection est décrite ci-dessous
public namespace System.Collections.Generic
{
interface ICollection<T> : IEnumerable<T>, IEnumerable
{
int Count { get; } //
Nombre d'éléments
bool IsReadOnly { get; }
void Add(T item);
void Clear(); //
//
//
Est en lecture seule
Ajoute un élément item
Supprime tous les éléments du
bool Contains(T item);
//
Est-ce que item est présent
void CopyTo(T[] array, int arrayIndex); // Copie dans un tableau
bool Remove(T item); // supprime l'élément item
}
59
C#/.NET
Interface IList<T>
Plus riche que l'interface ICollection, l'interface IList<T> permet d'atteindre des éléments par un index
(comme un tableau). La classe générique List<T> implémente cette interface.
namespace System.Collections.Generic
{
public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable
{
T this[int index] { get; set; } // indexeur
int IndexOf(T item); // donne l'index de item ou -1 sinon
void Insert(int index, T item); // insertion de item à l'index
void RemoveAt(int index); // supprime l'élment à index
}
}
Interface IDictionary<TKey,TValue>
C'est l'interface pour les structures de données (clé,valeur). On appelle parfois celà des tableaux associatifs. On
peut accéder aux élements en fournissant non pas un indice, mais une clé.
namespace System.Collections.Generic
{
public interface IDictionary<TKey, TValue> : ICollection<KeyValuePair<TKey, TValue>>,
IEnumerable<KeyValuePair<TKey, TValue>>, IEnumerable
{
ICollection<TKey> Keys { get; }
// fournit la collection des clés
ICollection<TValue> Values { get; }
// les valeurs
TValue this[TKey key] { get; set; }
void Add(TKey key, TValue value);
// valeur de clé key en lecture/écriture
// ajoute une valeur pour la clé key
bool ContainsKey(TKey key); // recherche présence de la clé
bool Remove(TKey key);
// supprime la clé
bool TryGetValue(TKey key, out TValue value);
}
}
11.3.2.
List<T>
La classe générique List<T> permet de générer des tableaux (et non des listes chaînées) dont la taille peut être
dynamiquement modifiée par l'ajout ou la suppression. Rappelons que les tableaux (Array) eux ne sont pas
redimensionnables. Cette classe générique implémente l'interface IList<T>.
namespace System.Collections.Generic
{
public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>,
IList, ICollection, IEnumerable
{
public List(); // tableau vide de capacité par défaut
public List(IEnumerable<T> collection); // tableau initialisé avec collection
public List(int capacity); // tableau vide de capacité fournie
public int Capacity { get; set; }
// taille mémoire disponible
60
C#/.NET
// avant reallocation
public int Count { get; }
// nombre d'éléments
public T this[int index] { get; set; } // indexeur
public void Add(T item);
public void AddRange(IEnumerable<T> collection); // ajout en fin
public int BinarySearch(T item); // recherche dans collection triée
public int BinarySearch(T item, IComparer<T> comparer); // idem
public int BinarySearch(int index, int count, T item, IComparer<T> comparer);
public void Clear(); // supprime tous les éléments
public bool Contains(T item); // teste la présence de item
public void CopyTo(T[] array); // copie vers array
public void CopyTo(T[] array, int arrayIndex); // à partir de arrayIndex
public void CopyTo(int index, T[] array, int arrayIndex, int count);
public bool Exists(Predicate<T> match); // teste le prédicat sur la collection
public T Find(Predicate<T> match); // premiere occurrence qui vérifie match
public List<T> FindAll(Predicate<T> match); // éléments qui vérifient match
public int FindIndex(Predicate<T> match); // index du premier qui matche
public int FindIndex(int startIndex, Predicate<T> match);
public int FindIndex(int startIndex, int count, Predicate<T> match);
public T FindLast(Predicate<T> match);
public int FindLastIndex(Predicate<T> match);
public int FindLastIndex(int startIndex, Predicate<T> match);
public int FindLastIndex(int startIndex, int count, Predicate<T> match);
public void ForEach(Action<T> action); // execute Action<T> sur tous
public List<T>.Enumerator GetEnumerator(); // obtient un Enumerator
public List<T> GetRange(int index, int count); // retourne une partie
public int IndexOf(T item); // indice de item ou -1
public int IndexOf(T item, int index); // indice de item à partir de index
public int IndexOf(T item, int index, int count);
public void Insert(int index, T item);
// insère item à l'indice index
public void InsertRange(int index, IEnumerable<T> collection);
public int LastIndexOf(T item);
public int LastIndexOf(T item, int index);
public int LastIndexOf(T item, int index, int count);
public
public
public
public
bool Remove(T item);
int RemoveAll(Predicate<T> match);
void RemoveAt(int index);
void RemoveRange(int index, int count);
public void Reverse(); // inverse l'ordre des éléments
public void Reverse(int index, int count); // idem dans l'intervalle spécifié
public void Sort(); // trie
public void Sort(IComparer<T> comparer); // trie selon comparateur fourni
public void Sort(int index, int count, IComparer<T> comparer);
public T[] ToArray();
// fournit les éléments dans un tableau
}
11.3.3.
LinkedList<T>
Les objets de type LinkedList<T> sont des listes doublement chaînées d'éléments de type T.
61
C#/.NET
Cette classe n'implémente pas IList<T> puisque l'on ne peut pas accéder efficacement directement à un
élément.
Les éléments sont stockés dans des noeuds dont la structure est décrite ci-dessous.
namespace System.Collections.Generic
{
public sealed class LinkedListNode<T>
{
public LinkedListNode(T value);
public LinkedList<T> List { get; }
public LinkedListNode<T> Next { get; }
public LinkedListNode<T> Previous { get; }
public T Value { get; set; }
}
}
Les méthodes de la classe LinkedList<T> sont décrites ci-dessous
namespace System.Collections.Generic
{
public class LinkedList<T> : ICollection<T>, IEnumerable<T>,
ICollection, IEnumerable, ISerializable,
IDeserializationCallback
{
public LinkedList(); // crée Liste vide
public LinkedList(IEnumerable<T> collection);
protected LinkedList(SerializationInfo info, StreamingContext context);
public int Count { get; } // nombre d'éléments
public LinkedListNode<T> First { get; } // premier et dernier noeud
public LinkedListNode<T> Last { get; }
public void AddAfter(LinkedListNode<T> node,
LinkedListNode<T> newNode);
public LinkedListNode<T> AddAfter(LinkedListNode<T> node, T value);
public void AddBefore(LinkedListNode<T> node,
LinkedListNode<T> newNode);
public LinkedListNode<T> AddBefore(LinkedListNode<T> node, T value);
public void AddFirst(LinkedListNode<T> node);
public LinkedListNode<T> AddFirst(T value);
public void AddLast(LinkedListNode<T> node);
public LinkedListNode<T> AddLast(T value);
public void Clear(); // supprime tous les noeuds
public bool Contains(T value); // indique présence de value
public
public
public
public
void CopyTo(T[] array, int index);
LinkedListNode<T> Find(T value);
LinkedListNode<T> FindLast(T value);
LinkedList<T>.Enumerator GetEnumerator();
public
public
public
public
void
bool
void
void
Remove(LinkedListNode<T> node);
Remove(T value);
RemoveFirst(); // supprime le premier noeud
RemoveLast();
// supprime le dernier
}
}
62
C#/.NET
11.3.4. Classe Dictinary<TKey,TValue> (tableau associatif (clé,valeur))
Classe générique de tableaux associatifs implémentant l'interface IDictionary<key,value>.
namespace System.Collections.Generic
{
public class Dictionary<TKey, TValue> : IDictionary<TKey, TValue>,
ICollection<KeyValuePair<TKey, TValue>>,
IEnumerable<KeyValuePair<TKey, TValue>>,
IDictionary, ICollection, IEnumerable, Iserializable,
IDeserializationCallback
{
public Dictionary();
public Dictionary(IDictionary<TKey, TValue> dictionary);
public int Count { get; }
public Dictionary<TKey, TValue>.KeyCollection Keys { get; }
public Dictionary<TKey, TValue>.ValueCollection Values { get; }
public
public
public
public
public
public
public
TValue this[TKey key] { get; set; }
void Add(TKey key, TValue value);
void Clear();
bool ContainsKey(TKey key);
Dictionary<TKey, TValue>.Enumerator GetEnumerator();
bool Remove(TKey key);
bool TryGetValue(TKey key, out TValue value);
}
}
63
C#/.NET
11.4. Table des matières
1.Introduction...........................................................................................................................................................................3
2.Les héritages du langage C...................................................................................................................................................4
2.1.Premier exemple console............................................................................................................................................4
2.2.Types primitifs du C# (alias de types .NET)...........................................................................................................5
2.3.Littérals (constantes) / Format des constantes.......................................................................................................5
2.4.Dépassements ..............................................................................................................................................................6
2.5.Les fonctions et les passages de paramètres (ref, out)............................................................................................7
2.6. Définition d'un type structuré (similaire aux structures du C).............................................................................8
2.7.Type structuré (struct) avec des fonctions membres ...........................................................................................9
3.La programmation orientée objet en C#..........................................................................................................................9
3.1.Le paradigme objet en bref (vocabulaire).................................................................................................................9
3.2.Le mot clé class ( class vs struct).............................................................................................................................10
3.3.Types valeurs / Types référence..............................................................................................................................11
3.4.Distinctions Types valeur/Types référence concernant l'affectation................................................................12
3.5.Distinction type valeur/type référence concernant l'égalité (==)......................................................................13
4.L'écriture de classes en C# (types référence)..................................................................................................................13
4.1.Namespaces................................................................................................................................................................13
4.2.Accessibilité.................................................................................................................................................................15
4.3.Surcharge des méthodes............................................................................................................................................15
4.4.Champs constants / en lecture seule......................................................................................................................16
4.5.Objet courant (référence this)..................................................................................................................................16
4.6.Constructeurs (initialisation des objets)..................................................................................................................17
4.6.1.Un constructeur peut appeler un autre constructeur (mot clé this).........................................................18
4.7.Membres statiques (données ou méthodes) .........................................................................................................19
4.7.1.Constructeur statique.......................................................................................................................................19
4.8.Passage de paramètres aux méthodes.....................................................................................................................20
4.8.1.Passage par référence (rappel).........................................................................................................................20
4.8.2.Méthodes avec un nombre variable de paramètres.....................................................................................20
4.9.Distinction des attributs de type valeur et de type référence..............................................................................21
4.10.Propriétés..................................................................................................................................................................21
4.11. Indexeur...................................................................................................................................................................22
4.12.Définition des opérateurs en C#
..................................................................................................................23
4.13.Conversion Numérique ↔ Chaîne (format/parse)...........................................................................................24
4.13.1.Formatage (conversion numérique vers string).........................................................................................24
4.13.2.Parsing (conversion string vers numérique)...............................................................................................25
4.14.Les énumérations (types valeur)............................................................................................................................25
4.15.Les structures............................................................................................................................................................26
4.16.Notation UML des classes......................................................................................................................................27
5.La composition (objet avec des objets membres)..........................................................................................................27
6.La dérivation .......................................................................................................................................................................28
6.1.Syntaxe.........................................................................................................................................................................30
6.2.Différence de visibilité protected/private..............................................................................................................30
6.3.Initialisateur de constructeur (dans le cas de la dérivation) ................................................................................30
6.4. Par défaut, les méthodes ne sont pas virtuelles (ligature statique)....................................................................31
6.5.Définition des méthodes virtuelles (méthodes à ligature dynamique)...............................................................31
6.6.Une propriété peut être surdéfinie dans la classe dérivée....................................................................................32
6.7.Classe object (System.Object)..................................................................................................................................33
6.8.Boxing/unboxing.......................................................................................................................................................34
6.9.Reconnaissance de type (runtime)...........................................................................................................................35
6.10.Exemple de synthèse sur la dérivation................................................................................................................35
6.11.Conversions de type ...............................................................................................................................................36
64
C#/.NET
6.11.1.Notation des casts...........................................................................................................................................36
6.11.2.Opérateur as.....................................................................................................................................................36
6.11.3.Opérateur is....................................................................................................................................................37
6.11.4.Typeof/Sizeof.................................................................................................................................................37
7.Interfaces et classes abstraites...........................................................................................................................................37
7.1.Interfaces.....................................................................................................................................................................37
7.1.1.Les méthodes d'une interface ne sont pas virtuelles...................................................................................38
7.1.2.Forcer à utiliser l'abstraction...........................................................................................................................39
7.1.3.Interfaces standard .NET................................................................................................................................39
7.1.4.Tester qu'un objet implémente une interface (opérateurs is et as)............................................................40
7.2.Classes abstraites........................................................................................................................................................40
8.Exceptions............................................................................................................................................................................41
9.Divers....................................................................................................................................................................................44
9.1.Types imbriqués.........................................................................................................................................................44
9.2.Dérivation et vérification de type ...........................................................................................................................44
9.3.Accès aux membres non virtuels hérités ...............................................................................................................46
9.4.Désactiver le polymorphisme dans une hiérarchie de classes.............................................................................46
10.Délégation (delegate) et Evénements (event)..............................................................................................................47
10.1.Délégation.................................................................................................................................................................48
10.2.Plusieurs appels pour un délégué..........................................................................................................................49
10.3.Appels d'une méthode d'une classe par un délégué...........................................................................................49
10.4.Evénéments..............................................................................................................................................................50
10.5.Exploitation standard des événements dans .NET............................................................................................51
11.Les classes conteneurs .....................................................................................................................................................52
11.1.Classe string..............................................................................................................................................................52
11.1.1.Les membres de la classe string ...................................................................................................................52
11.1.2.Le formattage de types numériques.............................................................................................................54
11.2.Les tableaux..............................................................................................................................................................56
11.2.1.Tableau unidimensionnel...............................................................................................................................56
11.2.2.Parcours d'un tableau.....................................................................................................................................56
11.2.3.Tableau à plusieurs dimensions....................................................................................................................56
11.2.4.Tableaux déchiquetés (jagged arrays) ..........................................................................................................57
11.2.5.Tableaux hétérogènes....................................................................................................................................57
11.2.6.Copie de tableaux ...........................................................................................................................................57
11.2.7.Membres de la classe Array...........................................................................................................................57
11.3.Les conteneurs génériques......................................................................................................................................58
11.3.1.Quelques interfaces de conteneurs : IEnumerable, ICollection, Ilist, IDictionary.............................59
11.3.2. List<T>...........................................................................................................................................................60
11.3.3. LinkedList<T>..............................................................................................................................................61
11.3.4.Classe Dictinary<TKey,TValue> (tableau associatif (clé,valeur))..........................................................63
65