Nous allons utiliser la classe CompteBancaire ci-dessous, issue d'un précédent chapitre.
// Définit un compte bancairepublicclassCompteBancaire{privatestringtitulaire;// Titulaire du compteprivatedoublesolde;// Solde du compteprivatestringdevise;// Devise du comptepublicstringTitulaire{get{returntitulaire;}}publicdoubleSolde{get{returnsolde;}}publicstringDevise{get{returndevise;}} // ConstructeurpublicCompteBancaire(stringleTitulaire,doublesoldeInitial,stringlaDevise){titulaire=leTitulaire;solde=soldeInitial;devise=laDevise;} // Ajoute un montant au comptepublicvoidCrediter(doublemontant){solde=solde+montant;} // Retire un montant au comptepublicvoidDebiter(doublemontant){solde=solde-montant;} // Renvoie la description du comptepublicstringDecrire(){stringdescription="Le solde du compte de "+titulaire+" est de "+solde+""+devise;returndescription;}}
Le programme de test associé est le suivant.
Résultat de l'exécution
Autoréférence : le mot-clé this
DEFINITION : à 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.
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. Par contre, il n'y a pas d'ambiguïté entre l'attribut soldeet le paramètre soldeInitial qui porte un nom différent.
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.
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. Etant donné que CompteBancaire n'hérite a priori d'aucune classe, on s'attend à ce que cette liste soit vide.
Redéfinition de la méthode ToString sous Visual Studio
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.
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.
DEFINITION : 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.
Résultat de l'exécution
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.
A présent, nous allons redéfinir la méthode ToString pour qu'elle renvoie des informations plus utiles sur le compte.
Résultat de l'exécution
La méthode ToStringest 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.
Le résultat est identique au précédent.
Résultat de l'exécution
REMARQUE : la méthode Decrire, qui fait doublon avec ToString, peut maintenant être supprimée.
Surcharge de méthodes
Définition de plusieurs constructeurs
La classe CompteBancaire possède un constructeur qui initialise les attributs du compte créé.
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.
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.
Autocomplétion sous Visual Studio
Autocomplétion sous Visual Studio
Cela signifie qu'il existe à présent deux constructeurs, donc deux manières d'instancier un compte. On choisit d'utiliser le nouveau constructeur.
Résultat de l'exécution
On constate que tous les attributs du compte ont été correctement initialisés.
Chaînage de constructeurs
Revenons à notre nouveau constructeur. Il est possible d'améliorer sa définition de la manière suivante.
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 qu'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.
En diversifiant la manière dont un objet peut être instancié, la présence de plusieurs constructeurs facilite l'utilisation d'une classe,
Surcharge d'une méthode
Le mécanisme qui consiste à définir plusieurs constructeurs avec des signatures différentes s'appelle la surcharge.
DEFINITION : 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.
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.
Surcharge de la méthode Crediter
DEFINITION : 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'oeuvre dans les classes du framework .NET. A titre d'exemple, la méthode Console.WriteLine totalise ci-dessous 19 signatures différentes.
Surcharge de la méthode WriteLine
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.
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.
La définition de cette méthode provoque une erreur de compilation.
Erreur de surcharge
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.
Membres de classe
Evolution des besoins
Imaginons qu'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.
Diagramme UML de la classe CompteBancaire
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 de 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 serait attribué au compte en cours de création, puis incrémenté.
Essayons de mettre en oeuvre cette stratégie.
Cependant, on constate un problème lorsque l'on instancie plusieurs objets.
Résultat de l'exécution
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.
Représentation mémoire des comptes bancaires
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.
Le même programme principal donne à présent un résultat différent.
Résultat de l'exécution
Ce résultat illustre le fait que l'attribut numeroProchainCompte est maintenant lié à la classe et partagé entre toutes ses instances. A chaque appel du constructeur, sa valeur courante est récupérée puis incrémentée. On obtient le comportement désiré.
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 qu'on souhaite pouvoir récupérer le numéro du prochain compte. Il faut ajouter une méthode à la classe CompteBancaire.
On peut modifier le programme principal afin d'exploiter cette nouvelle méthode.
Résultat de l'exécution
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. Etant 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.
CompteBancaire compte = new CompteBancaire("Pierre", 100, "dollars");
Console.WriteLine(compte.ToString());
namespace Chap6_Complements
{
public class CompteBancaire
{
// ...
public class CompteBancaire
{
// ...
public override string ToString()
{
return "Le solde du compte de " + titulaire + " est de " + solde + " " + devise;
}
}
CompteBancaire compte = new CompteBancaire("Pierre", 100, "dollars");
Console.WriteLine(compte);
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;
}
}
CompteBancaire comptePierre = new CompteBancaire("Pierre");
CompteBancaire comptePaul = new CompteBancaire("Paul");
CompteBancaire compteJacques = new CompteBancaire("Jacques");
Console.WriteLine(comptePierre);
Console.WriteLine(comptePaul);
Console.WriteLine(compteJacques);
public class CompteBancaire
{
// ...
private int numero;
private static int numeroProchainCompte = 1; // Numéro du prochain compte créé
// ...
// ...
public static int GetNumeroProchainCompte()
{
return numeroProchainCompte;
}
// ...
Console.WriteLine("Avant la 1ère création : " + CompteBancaire.GetNumeroProchainCompte());
CompteBancaire comptePierre = new CompteBancaire("Pierre");
Console.WriteLine("Avant la 2ème création : " + CompteBancaire.GetNumeroProchainCompte());
CompteBancaire comptePaul = new CompteBancaire("Paul");
Console.WriteLine("Avant la 3ème création : " + CompteBancaire.GetNumeroProchainCompte());
CompteBancaire compteJacques = new CompteBancaire("Jacques");
Console.WriteLine("Après la 3ème création : " + CompteBancaire.GetNumeroProchainCompte());
Console.WriteLine(comptePierre);
Console.WriteLine(comptePaul);
Console.WriteLine(compteJacques);