VII. Compléments sur l'écriture de classes▲
L'objectif de ce chapitre est de découvrir comment enrichir la définition de nos classes en utilisant certaines possibilités des langages à objets.
Les exemples de code associés sont disponibles en ligne.
VII-A. Contexte d'exemple▲
Nous allons utiliser la classe CompteBancaire ci-dessous, issue d'un précédent chapitre.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
// Définit un compte bancaire
public
class
CompteBancaire
{
private
string
titulaire;
// Titulaire du compte
private
double
solde;
// Solde du compte
private
string
devise;
// Devise du compte
public
string
Titulaire
{
get
{
return
titulaire;
}
}
public
double
Solde
{
get
{
return
solde;
}
}
public
string
Devise
{
get
{
return
devise;
}
}
// Constructeur
public
CompteBancaire
(
string
leTitulaire,
double
soldeInitial,
string
laDevise)
{
titulaire =
leTitulaire;
solde =
soldeInitial;
devise =
laDevise;
}
// Ajoute un montant au compte
public
void
Crediter
(
double
montant)
{
solde =
solde +
montant;
}
// Retire un montant au compte
public
void
Debiter
(
double
montant)
{
solde =
solde -
montant;
}
// Renvoie la description du compte
public
string
Decrire
(
)
{
string
description =
"Le solde du compte de "
+
titulaire +
" est de "
+
solde +
" "
+
devise;
return
description;
}
}
Le programme de test associé est le suivant.
CompteBancaire compte =
new
CompteBancaire
(
"Pierre"
,
100
,
"dollars"
);
Console.
WriteLine
(
compte.
Decrire
(
));
VII-B. Autoréférence : le mot-clé this▲
DÉFINITION : à l'intérieur d'une méthode, le mot-clé this
permet d'accéder à l'instance (l'objet) sur lequel la méthode est appelée.
L'une des utilisations fréquentes de this
consiste à lever les ambiguïtés de nommage entre attributs et paramètres. Par exemple, on pourrait réécrire le constructeur de CompteBancaire en changeant les noms de ses paramètres de la manière suivante.
2.
3.
4.
5.
6.
public
CompteBancaire
(
string
titulaire,
double
soldeInitial,
string
devise)
{
this
.
titulaire =
titulaire;
solde =
soldeInitial;
this
.
devise =
devise;
}
Ici, l'expression this
.
titulaire désigne sans ambiguïté l'attribut de la classe, alors que l'expression titulaire désigne le paramètre du constructeur. Il n'y a pas non plus d'ambiguïté entre l'attribut solde et le paramètre soldeInitial qui porte un nom différent.
VII-C. Utilisation des méthodes de base▲
Nous allons voir que toute classe C# dispose d'un certain nombre de méthodes de base que nous pouvons exploiter.
VII-C-1. Découverte de la classe Object▲
Utilisons une fonctionnalité de Visual Studio pour découvrir si des méthodes sont redéfinissables dans CompteBancaire. En tapant le mot-clé override
dans une classe, l'IDE nous propose la liste des méthodes de cette classe qu'il est possible de redéfinir. Étant donné que CompteBancaire n'hérite a priori d'aucune classe, on s'attend à ce que cette liste soit vide.
L'IDE nous propose pourtant trois méthodes à redéfinir : Equals, GetHashCode et ToString. Ceci s'explique par le fait que toute classe C# qui n'hérite explicitement d'aucune autre, hérite implicitement d'une classe de base nommée Object. Ce design est inspiré de celui du langage Java.
La documentation Microsoft donne plus de précisions sur cette classe : « Il s'agit de la classe de base fondamentale parmi toutes les classes du .NET Framework. Elle constitue la racine de la hiérarchie des types. »
La classe Object dispose de plusieurs méthodes. Toute classe C# hérite directement ou indirectement de cette classe et peut utiliser ou redéfinir ses méthodes.
VII-C-2. Redéfinition de ToString▲
Parmi les méthodes présentes dans la classe Object, la plus souvent redéfinie dans les classes dérivées est ToString.
DÉFINITION : la méthode ToString permet de décrire un objet sous la forme d'une chaîne de caractères.
REMARQUE : examinons tout d'abord le comportement par défaut (sans redéfinition) de cette méthode.
CompteBancaire compte =
new
CompteBancaire
(
"Pierre"
,
100
,
"dollars"
);
Console.
WriteLine
(
compte.
ToString
(
));
Par défaut, la méthode ToString affiche le nom complet de la classe. Ici, Chap6_Complements désigne l'espace de noms (namespace) dans lequel la classe CompteBancaire est définie.
2.
3.
4.
5.
namespace
Chap6_Complements
{
public
class
CompteBancaire
{
// ...
À présent, nous allons redéfinir la méthode ToString pour qu'elle renvoie des informations plus utiles sur le compte.
2.
3.
4.
5.
6.
7.
8.
9.
public
class
CompteBancaire
{
// ...
public
override
string
ToString
(
)
{
return
"Le solde du compte de "
+
titulaire +
" est de "
+
solde +
" "
+
devise;
}
}
La méthode ToString est utilisée implicitement lorsqu'un objet est affiché dans la console ou dans un contrôle graphique WinForms (liste déroulante, etc.). On peut donc simplifier le programme principal en passant simplement l'objet à afficher à Console.WriteLine.
CompteBancaire compte =
new
CompteBancaire
(
"Pierre"
,
100
,
"dollars"
);
Console.
WriteLine
(
compte);
Le résultat est identique au précédent.
REMARQUE : la méthode Decrire, qui fait doublon avec ToString, peut maintenant être supprimée.
VII-D. Surcharge de méthodes▲
VII-D-1. Définition de plusieurs constructeurs▲
La classe CompteBancaire possède un constructeur qui initialise les attributs du compte créé.
2.
3.
4.
5.
6.
public
CompteBancaire
(
string
titulaire,
double
soldeInitial,
string
devise)
{
this
.
titulaire =
titulaire;
solde =
soldeInitial;
this
.
devise =
devise;
}
Ce constructeur nous oblige à définir systématiquement le titulaire, le solde et la devise de tout nouveau compte. Or, les nouveaux comptes bancaires disposent le plus souvent d'un solde égal à zéro et utilisent l'euro comme devise. Pour refléter cet aspect du domaine et faciliter l'utilisation de notre classe, on va ajouter un autre constructeur. Il prendra uniquement le titulaire en paramètre, et initialisera solde et devise avec leurs valeurs par défaut.
2.
3.
4.
5.
6.
public
CompteBancaire
(
string
titulaire)
{
this
.
titulaire =
titulaire;
solde =
0
;
devise =
"euros"
;
}
On peut à présent instancier un compte bancaire en utilisant ce constructeur. On remarque que Visual Studio nous propose deux choix d'autocomplétion, visualisables à l'aide de petites flèches noires.
Cela signifie qu'il existe à présent deux constructeurs, donc deux manières d'instancier un compte. On choisit d'utiliser le nouveau constructeur.
CompteBancaire compte =
new
CompteBancaire
(
"Paul"
);
Console.
WriteLine
(
compte);
On constate que tous les attributs du compte ont été correctement initialisés.
VII-D-2. Chaînage de constructeurs▲
Revenons à notre nouveau constructeur. Il est possible d'améliorer sa définition de la manière suivante.
2.
3.
4.
public
CompteBancaire
(
string
titulaire) :
this
(
titulaire,
0
,
"euros"
)
{
// Rien à faire !
}
Cette variante n'initialise pas directement les attributs, mais fait appel à l'autre constructeur en lui passant en paramètres des valeurs par défaut pour les attributs que lui-même n'a pas reçus en paramètres.
CONSEIL : le chaînage des constructeurs les uns aux autres est une bonne pratique pour éviter la duplication du code d'initialisation des attributs.
Imaginons que l'on souhaite pouvoir instancier un compte bancaire en définissant son titulaire et son solde (la devise par défaut étant toujours l'euro). On va ajouter à notre classe un troisième constructeur en les chaînant les uns aux autres. Voici les trois constructeurs de CompteBancaire.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
public
CompteBancaire
(
string
titulaire) :
this
(
titulaire,
0
)
{}
public
CompteBancaire
(
string
titulaire,
double
soldeInitial)
:
this
(
titulaire,
soldeInitial,
"euros"
)
{}
public
CompteBancaire
(
string
titulaire,
double
soldeInitial,
string
devise)
{
this
.
titulaire =
titulaire;
solde =
soldeInitial;
this
.
devise =
devise;
}
En diversifiant la manière dont un objet peut être instancié, la présence de plusieurs constructeurs facilite l'utilisation d'une classe,
VII-D-3. Surcharge d'une méthode▲
Le mécanisme qui consiste à définir plusieurs constructeurs avec des signatures différentes s'appelle la surcharge.
DÉFINITION : la signature d'une méthode comprend son nom et la liste de ses paramètres (nombre, types et ordre).
On peut également appliquer ce mécanisme pour définir plusieurs méthodes avec des signatures différentes. Par exemple, on pourrait vouloir créditer (ou débiter) un compte avec un montant exprimé dans une devise différente. La solution logique est de créer une nouvelle méthode Crediter (ou Debiter) prenant en paramètres montant et devise du montant.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
// ...
public
void
Crediter
(
double
montant)
{
solde +=
montant;
}
public
void
Crediter
(
double
montant,
string
devise)
{
if
(
devise ==
Devise)
Crediter
(
montant);
else
{
// TODO : gérer la conversion de devises
}
}
// ...
Les deux méthodes portent le même nom, mais leurs signatures (nom + paramètres) sont différentes, d'où l'absence d'erreur de compilation. Le comportement de l'IDE lors de l'utilisation de la nouvelle méthode reflète la présence de deux signatures pour la méthode Crediter.
DÉFINITION : la surcharge est un mécanisme qui consiste à définir plusieurs variantes d'une même méthode avec des signatures différentes.
Le mécanisme de surcharge permet de proposer plusieurs variantes d'un même comportement. On le trouve fréquemment à l'œuvre dans les classes du framework .NET. À titre d'exemple, la méthode Console.WriteLine totalise ci-dessous 19 signatures différentes.
ATTENTION : ne pas confondre surcharge et redéfinition.
- Surcharger (to overload) une méthode, c'est ajouter une méthode de même nom avec une signature différente.
- Redéfinir (to override) une méthode, c'est la réécrire (avec la même signature) dans une classe dérivée.
VII-D-4. Surcharge et valeur de retour▲
On pourrait envisager de surcharger la méthode Crediter afin de renvoyer le solde du compte après l'opération.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
// ...
public
void
Crediter
(
double
montant)
{
solde +=
montant;
}
public
double
Crediter
(
double
montant)
{
solde +=
montant;
return
solde;
}
// ...
La définition de cette méthode provoque une erreur de compilation.
Cette erreur s'explique par le fait que la signature d'une méthode (nom + paramètres) n'inclut pas son type de retour.
ATTENTION : il est impossible de surcharger une méthode en modifiant uniquement son type de retour.
VII-E. Membres de classe▲
VII-E-1. Evolution des besoins▲
Imaginons que l'on souhaite identifier les comptes bancaires par un numéro unique. Le premier compte créé porterait le numéro 1, le second le numéro 2, etc.
Une possibilité serait de passer le numéro de compte en paramètre au constructeur. Cependant, elle serait fastidieuse à utiliser (l'utilisateur de la classe doit gérer lui-même l'unicité et l'incrémentation des numéros) et source d'erreurs (aucun contrôle et risque de doublons dans les numéros).
Une meilleure solution consiste à internaliser la problématique de définition du numéro à l'intérieur de la classe CompteBancaire. Il faudrait stocker dans la classe l'information sur le numéro du prochain compte. Lors de la création du compte suivant (c'est-à-dire lors du prochain appel au constructeur de la classe CompteBancaire), le numéro du prochain compte lui serait attribué, puis incrémenté.
Essayons de mettre en œuvre cette stratégie.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
public
class
CompteBancaire
{
private
string
titulaire;
private
double
solde;
private
string
devise;
private
int
numero;
// numéro du compte
private
int
numeroProchainCompte =
1
;
// numéro du prochain compte créé
public
CompteBancaire
(
string
titulaire,
double
soldeInitial,
string
devise)
{
// ... (initialisation des attributs titulaire, solde et devise)
numero =
numeroProchainCompte;
numeroProchainCompte++;
}
// ...
public
override
string
ToString
(
)
{
return
"Le solde du compte numéro "
+
numero +
" de "
+
titulaire +
" est de "
+
solde +
" "
+
devise;
}
}
Cependant, on constate un problème lorsque l'on instancie plusieurs objets.
2.
3.
4.
5.
6.
7.
CompteBancaire comptePierre =
new
CompteBancaire
(
"Pierre"
);
CompteBancaire comptePaul =
new
CompteBancaire
(
"Paul"
);
CompteBancaire compteJacques =
new
CompteBancaire
(
"Jacques"
);
Console.
WriteLine
(
comptePierre);
Console.
WriteLine
(
comptePaul);
Console.
WriteLine
(
compteJacques);
Tous les comptes portent le même numéro ! Cela vient du fait que chaque instance de la classe CompteBancaire dispose de ses propres attributs. Chaque objet a donc un attribut numeroProchainCompte qui lui est propre, avec une valeur spécifique. L'incrémentation de cet attribut dans le constructeur, lors de l'instanciation d'un compte, n'a aucun effet sur les attributs des instances existantes. Voici les valeurs de ces attributs une fois les objets créés.
VII-E-2. Définition d'un attribut de classe▲
La solution serait de lier l'attribut numeroProchainCompte à la classe CompteBancaire elle-même, et non à chacune de ses instances. Le même attribut serait partagé entre toutes les instances de la classe, ce qui permettrait d'obtenir des numéros de compte uniques.
On parle d'attributs de classe, par opposition aux attributs appelés attributs d'instance quand on veut distinguer les deux types d'attributs.
En C# (ainsi qu'en Java et en C++), la création d'un attribut de classe se fait en précisant sa définition grâce au mot-clé static
, que vous avez déjà rencontré par ailleurs.
2.
3.
4.
5.
6.
7.
public
class
CompteBancaire
{
// ...
private
int
numero;
private
static
int
numeroProchainCompte =
1
;
// Numéro du prochain compte créé
// ...
Le même programme principal donne à présent un résultat différent.
Ce résultat illustre le fait que l'attribut numeroProchainCompte est maintenant lié à la classe et partagé entre toutes ses instances. À chaque appel du constructeur, sa valeur courante est récupérée puis incrémentée. On obtient le comportement désiré.
VII-E-3. Définition d'une méthode de classe▲
Ce qui vient d'être vu pour les attributs s'applique également aux méthodes. En utilisant le mot-clé static
, on définit une méthode de classe, par opposition aux méthodes habituelles appelées méthodes d'instance pour les distinguer.
Imaginons que l'on souhaite pouvoir récupérer le numéro du prochain compte. Il faut ajouter une méthode à la classe CompteBancaire.
2.
3.
4.
5.
6.
7.
8.
// ...
public
static
int
GetNumeroProchainCompte
(
)
{
return
numeroProchainCompte;
}
// ...
On peut modifier le programme principal afin d'exploiter cette nouvelle méthode.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
Console.
WriteLine
(
"Avant la 1re création : "
+
CompteBancaire.
GetNumeroProchainCompte
(
));
CompteBancaire comptePierre =
new
CompteBancaire
(
"Pierre"
);
Console.
WriteLine
(
"Avant la 2e création : "
+
CompteBancaire.
GetNumeroProchainCompte
(
));
CompteBancaire comptePaul =
new
CompteBancaire
(
"Paul"
);
Console.
WriteLine
(
"Avant la 3e création : "
+
CompteBancaire.
GetNumeroProchainCompte
(
));
CompteBancaire compteJacques =
new
CompteBancaire
(
"Jacques"
);
Console.
WriteLine
(
"Après la 3e création : "
+
CompteBancaire.
GetNumeroProchainCompte
(
));
Console.
WriteLine
(
comptePierre);
Console.
WriteLine
(
comptePaul);
Console.
WriteLine
(
compteJacques);
Une méthode de classe s'utilise de manière différente d'une méthode d'instance. Elle peut s'utiliser en l'absence de toute instance de la classe. Étant donné qu'elle est liée à la classe et non à ses instances, on préfixe le nom de la méthode par le nom de la classe, par exemple CompteBancaire.GetNumeroProchainCompte().
Une méthode de classe sert à définir un comportement indépendant de toute instance. Vous en avez déjà rencontrées certaines en utilisant les classes du framework .NET, par exemple Console.WriteLine ou Convert.ToDouble.
REMARQUE : la méthode Main, point d'entrée dans une application console en C#, est en réalité une méthode de classe de la classe Program.
Il existe des contraintes, somme toute logiques, concernant les interactions entre membres de classe et membres d'instance.
- Une méthode d'instance peut utiliser un membre de classe (attribut ou méthode).
- Une méthode de classe peut utiliser un membre de classe.
- Une méthode de classe ne peut pas utiliser un membre d'instance (attribut ou méthode).
- Une méthode de classe ne peut pas utiliser le mot-clé
this
.