Chapitre 720 minIntermédiaire
Héritage en C# : classe mère et classe fille
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- écrire une classe dérivée et appeler le constructeur parent avec
base; - dire ce qu'une classe fille hérite et ce qu'elle peut utiliser ;
- tester le type réel d'un objet avec
is,asetswitch; - choisir entre héritage et composition.
4 exercices corrigés1 schéma
L'héritage est le mécanisme qui permet de définir une classe à partir d'une autre : la classe fille (ou dérivée) reçoit les champs, propriétés et méthodes de la classe mère (ou classe de base), et ajoute ou adapte ce qui lui est propre. En C#, on l'écrit avec deux-points : class Portable : Poste. Ce chapitre montre quand hériter, comment appeler le constructeur de la classe mère avec base, ce que change protected, comment tester le type réel d'un objet, et quand préférer la composition.
Pourquoi l'héritage
Le parc de Sorbier Logistique compte des postes fixes, des portables et des imprimantes. Tous ont un numéro d'inventaire, une marque, un modèle et un prix d'achat ; mais un poste a un utilisateur, un portable peut avoir un disque chiffré, une imprimante imprime ou non en couleur. Écrire trois classes indépendantes recopierait quatre propriétés et leurs règles dans chacune : une correction oubliée dans l'une, et le parc devient incohérent.
L'héritage factorise ce qui est commun dans une classe mère, Materiel, et ne laisse dans chaque classe fille que ce qui la distingue. On le teste avec la phrase « est un » : un portable est un poste, un poste est un matériel, une imprimante est un matériel. Si la phrase sonne faux, l'héritage est mauvais.
| Vocabulaire | Synonymes |
|---|---|
| Classe mère | classe de base, classe parente, super-classe, classe générale |
| Classe fille | classe dérivée, sous-classe, classe spécialisée |
| Hériter de | dériver de, spécialiser, étendre (extends en Java et en PHP) |
| Relation d'héritage en UML | généralisation : flèche à triangle vide, de la fille vers la mère |
Écrire une classe dérivée
class Materiel
{
public Materiel(string numInventaire, string marque, string modele, decimal prixAchat)
{
NumInventaire = numInventaire;
Marque = marque;
Modele = modele;
PrixAchat = prixAchat;
}
public string NumInventaire { get; }
public string Marque { get; }
public string Modele { get; }
public decimal PrixAchat { get; }
protected bool EnReparation { get; set; } // visible des classes filles, pas de l'extérieur
public string Etiquette() => $"{NumInventaire} – {Marque} {Modele}";
}
class Poste : Materiel
{
public Poste(string numInventaire, string marque, string modele, decimal prixAchat, string? utilisateur)
: base(numInventaire, marque, modele, prixAchat)
{
Utilisateur = utilisateur;
}
public string? Utilisateur { get; set; }
}
class Imprimante : Materiel
{
public Imprimante(string numInventaire, string marque, string modele, decimal prixAchat, bool couleur)
: base(numInventaire, marque, modele, prixAchat)
{
Couleur = couleur;
}
public bool Couleur { get; }
}
sealed class Portable : Poste
{
public Portable(string numInventaire, string marque, string modele, decimal prixAchat, string? utilisateur, bool chiffre)
: base(numInventaire, marque, modele, prixAchat, utilisateur)
{
Chiffre = chiffre;
}
public bool Chiffre { get; }
public bool PeutSortirDuSite() => Chiffre && !EnReparation;
}
Poste fixe = new Poste("PC-0001", "Dell", "OptiPlex 7020", 899m, "Hélène Garnier");
Portable portable = new Portable("PC-0002", "Lenovo", "ThinkPad T14", 1290m, "Karim Morel", chiffre: true);
Imprimante imp = new Imprimante("IM-0001", "Brother", "HL-L8360", 529m, couleur: true);
Console.WriteLine(fixe.Etiquette());
Console.WriteLine(portable.Etiquette());
Console.WriteLine(imp.Etiquette());
Console.WriteLine(portable.Utilisateur); // hérité de Poste
Console.WriteLine(portable.Marque); // hérité de Materiel
Console.WriteLine(portable.PeutSortirDuSite());
PC-0001 – Dell OptiPlex 7020
PC-0002 – Lenovo ThinkPad T14
IM-0001 – Brother HL-L8360
Karim Morel
Lenovo
True
Etiquette() est écrite une seule fois, dans Materiel, et fonctionne pour les trois types. Portable hérite de Poste, qui hérite de Materiel : un portable a donc tout ce qu'a un poste, plus son chiffrement.
Le constructeur de la classe fille : base(...)
Les constructeurs ne s'héritent pas. Chaque classe fille écrit les siens, et chacun doit appeler un constructeur de la classe mère avec : base(...), pour que la partie « matériel » de l'objet soit initialisée et validée par la classe qui en est responsable.
Si tu oublies : base(...), C# appelle implicitement le constructeur sans paramètre de la classe mère. Materiel n'en a pas : la compilation échoue avec l'erreur CS7036, « Parmi les arguments spécifiés, aucun ne correspond au paramètre obligatoire 'numInventaire' de 'Materiel.Materiel(string, string, string, decimal)' ». C'est l'erreur numéro un des exercices d'héritage.
L'ordre d'exécution des constructeurs
À la création d'un Portable, les constructeurs s'exécutent de la classe la plus générale à la plus spécialisée : d'abord le corps de celui de Materiel, puis celui de Poste, enfin celui de Portable. Avec un Console.WriteLine dans chacun :
constructeur Materiel (PC-0003)
constructeur Poste
constructeur Portable
C'est logique : la classe fille peut compter sur une partie « mère » déjà prête quand son propre code s'exécute.
Ce qui est hérité, et le rôle de protected
Une classe fille hérite de tous les membres de sa mère, mais n'a pas accès à tous :
| Membre de la classe mère | Hérité ? | Utilisable dans la fille ? | Utilisable de l'extérieur ? |
|---|---|---|---|
public | oui | oui | oui |
protected | oui | oui | non |
private | oui (il existe dans l'objet) | non | non |
| constructeur | non | seulement via base(...) | via new sur la mère |
static | accessible par le nom de la classe | oui, s'il n'est pas privé | selon sa visibilité |
EnReparation est protected : Portable.PeutSortirDuSite() peut le lire, mais Program.cs ne peut pas écrire portable.EnReparation = true. Un champ private de Materiel existe bien dans chaque portable, mais seul le code de Materiel y touche ; la fille passe par les propriétés et méthodes publiques ou protégées.
Un objet de la classe fille est aussi un objet de la classe mère
Partout où le code attend un Materiel, on peut donner un Poste, un Portable ou une Imprimante. La conversion vers la classe mère est implicite et toujours sûre : c'est le principe de substitution.
Materiel m = portable; // un Portable est un Materiel
Console.WriteLine(m.Etiquette()); // PC-0002 – Lenovo ThinkPad T14
// m.Utilisateur // erreur : le type déclaré Materiel ne connaît pas Utilisateur
List<Materiel> parc = [fixe, portable, imp];
L'objet n'a pas changé : c'est toujours un portable. Mais la variable m est déclarée Materiel, et le compilateur ne laisse utiliser que ce que Materiel déclare. Le type de la variable décide de ce qu'on peut appeler ; le type de l'objet décide de ce qui s'exécute, comme le montrera le chapitre 8.
Une liste List<Materiel> peut ainsi mélanger postes, portables et imprimantes, et un seul foreach les traite tous.
Tester le type réel : is, as et le filtrage
Parfois, il faut savoir quel est l'objet réel derrière une variable de type mère.
Console.WriteLine(m is Poste); // True : un portable est un poste
Console.WriteLine(m is Imprimante); // False
if (m is Poste poste) // test et conversion en une fois
{
Console.WriteLine($"Utilisateur : {poste.Utilisateur}"); // Utilisateur : Karim Morel
}
Imprimante? i = m as Imprimante; // as : null si la conversion est impossible
Console.WriteLine(i is null); // True
Imprimante i2 = (Imprimante)m; // cast : InvalidCastException si impossible
| Écriture | Si le type ne correspond pas |
|---|---|
m is Poste | renvoie false |
m is Poste p | renvoie false, p n'est pas utilisable |
m as Poste | renvoie null |
(Poste)m | lève InvalidCastException |
Le switch sait aussi filtrer sur le type, et même sur des propriétés :
foreach (Materiel x in parc)
{
string type = x switch
{
Portable => "portable",
Poste => "poste fixe",
Imprimante { Couleur: true } => "imprimante couleur",
Imprimante => "imprimante",
_ => "autre"
};
Console.WriteLine($"{x.NumInventaire} : {type}");
}
PC-0001 : poste fixe
PC-0002 : portable
IM-0001 : imprimante couleur
L'ordre des cas compte : Portable doit venir avant Poste, sinon un portable serait pris pour un poste fixe. Le compilateur refuse d'ailleurs l'ordre inverse : le cas Portable placé après Poste ne pourrait jamais être atteint (erreur CS8510).
sealed : interdire l'héritage
sealed class Portable interdit de dériver de Portable. On scelle une classe quand rien ne justifie de la spécialiser, ou quand une spécialisation pourrait contourner ses règles. string est une classe scellée de .NET. Beaucoup d'équipes scellent leurs classes par défaut et ne les ouvrent qu'au besoin.
object, la mère de toutes les classes
Toute classe C# hérite, directement ou non, de System.Object (alias object). Une classe sans : hérite directement d'object. C'est pourquoi tout objet possède quatre méthodes, même si tu ne les as jamais écrites :
Méthode héritée d'object | Rôle | Redéfinie au |
|---|---|---|
ToString() | représentation texte ; par défaut, le nom complet du type | chapitre 8 |
Equals(object?) | égalité ; par défaut, des références | chapitre 11 |
GetHashCode() | code de hachage, utilisé par Dictionary et HashSet | chapitre 11 |
GetType() | le type réel de l'objet |
Console.WriteLine(fixe.GetType().BaseType?.Name); // Materiel
Console.WriteLine(typeof(Materiel).BaseType?.Name); // Object
Héritage simple, interfaces multiples
En C# (comme en Java), une classe n'a qu'une seule classe mère : class Portable : Poste, Imprimante est refusé. C'est l'héritage simple. Une classe peut en revanche implémenter plusieurs interfaces, ce qui couvre la plupart des besoins de l'héritage multiple sans ses ambiguïtés (chapitre 9). C++ autorise l'héritage multiple ; Python aussi.
Hériter ou composer ?
L'héritage est une relation forte et définitive : la classe fille dépend de tous les détails de sa mère. Avant d'hériter, applique deux tests.
- « Est un » ou « a un » ? Un technicien est un employé : héritage. Un poste a un utilisateur : association, pas héritage. Une voiture a un moteur : composition.
- Tout ce que fait la mère a-t-il du sens pour la fille ? Si
Imprimantehéritait dePostepour récupérerEtiquette(), elle hériterait aussi d'Utilisateuret de l'installation de logiciels, absurdes pour une imprimante. C'est le principe de substitution de Liskov (chapitre 12).
| Situation | Choix |
|---|---|
| Les classes partagent une vraie nature (« est un ») et des règles communes | héritage |
| Une classe veut seulement réutiliser une fonction d'une autre | composition : un champ qui contient l'autre objet, ou une méthode utilitaire |
| Un objet change de catégorie pendant sa vie (un employé devient technicien) | composition ou propriété : l'héritage fige le type à la création |
| Des classes sans lien de nature partagent une capacité (« peut être exporté ») | interface |
Exercices
Exercice 1 sur 4 Écrire une classe dérivée Réussi À revoir
La classe Employe existe, avec un constructeur Employe(string nom, string prenom), les propriétés Nom, Prenom, NomComplet et la méthode Presentation(). Écris la classe Technicien, qui hérite d'Employe et ajoute :
- une propriété
Specialite(texte, fixée à la création) ; - une liste privée de certifications, exposée en lecture seule ;
- une méthode
AjouterCertification(string intitule)qui met l'intitulé en majuscules et ignore un doublon.
Combien de certifications a Moussa Diallo, spécialiste réseau, après l'ajout de « CCNA », « ccna » et « ITIL 4 » ?
Voir la réponseMasquer la réponse de l'exercice 1
class Technicien : Employe
{
private readonly List<string> certifications = new List<string>();
public Technicien(string nom, string prenom, string specialite) : base(nom, prenom)
{
Specialite = specialite;
}
public string Specialite { get; }
public IReadOnlyList<string> Certifications => certifications;
public void AjouterCertification(string intitule)
{
intitule = intitule.Trim().ToUpper();
if (!certifications.Contains(intitule))
{
certifications.Add(intitule);
}
}
}
Moussa a 2 certifications : « ccna » devient « CCNA », déjà présent. t.Presentation(), héritée d'Employe, fonctionne sans être réécrite : « Moussa Diallo, salarié de Sorbier Logistique ».
Tu avais trouvé ?
Exercice 2 sur 4 Trouver les erreurs Réussi À revoir
Ce code ne compile pas. Trouve les trois erreurs.
class Materiel
{
private decimal prixAchat;
public Materiel(string numInventaire, decimal prixAchat) { /* … */ }
}
class Imprimante : Materiel
{
public Imprimante(string numInventaire, decimal prix, bool couleur)
{
Couleur = couleur;
}
public bool Couleur { get; }
public decimal CoutAnnuel() => prixAchat / 5 + 120m;
}
class Scanner : Imprimante, Materiel { }
Voir la réponseMasquer la réponse de l'exercice 2
- Le constructeur d'
Imprimanten'appelle pasbase(numInventaire, prix). C# cherche un constructeur sans paramètre dansMateriel, qui n'en a pas : erreur CS7036. Correction :public Imprimante(string numInventaire, decimal prix, bool couleur) : base(numInventaire, prix). CoutAnnuel()litprixAchat, un champprivatedeMateriel: erreur CS0122, inaccessible en raison de son niveau de protection. Correction : exposer une propriétépublic decimal PrixAchat { get; }(ouprotected) dansMaterielet l'utiliser.Scanner : Imprimante, Materiel: une classe ne peut avoir qu'une classe mère. Ici, c'est même inutile, puisqu'Imprimantehérite déjà deMateriel. Correction :class Scanner : Imprimante, avec son propre constructeur qui appellebase(...).
Tu avais trouvé ?
Exercice 3 sur 4 Est-un ou a-un ? Réussi À revoir
Pour chaque couple, choisis entre héritage, association (ou composition) et rien du tout.
AdministrateuretUtilisateurd'une application.TicketetIntervention.ServeuretMateriel.CompteBancaireetClient.StagiaireetEmploye, sachant qu'un stagiaire peut être embauché à la fin de son stage.
Voir la réponseMasquer la réponse de l'exercice 3
- Héritage possible : un administrateur est un utilisateur avec des droits en plus. Une propriété
Role(ou une liste de rôles) est souvent plus souple, car un utilisateur peut devenir administrateur. - Composition : un ticket a des interventions.
- Héritage : un serveur est un matériel, avec un numéro d'inventaire, un prix et un amortissement.
- Association : un client a un ou plusieurs comptes ; un compte n'est pas une sorte de client.
- Pas d'héritage figé : un objet ne change pas de classe. Si le stagiaire est embauché, il faudrait détruire l'objet
Stagiaireet en créer un autre. Mieux vaut une classeEmployeavec une propriétéTypeContrat(stage, CDD, CDI), ou une association vers un objetContrat.
Tu avais trouvé ?
Exercice 4 sur 4 Ordre des constructeurs Réussi À revoir
Qu'affiche new C(); ?
class A { public A() { Console.WriteLine("A"); } }
class B : A { public B() { Console.WriteLine("B"); } }
class C : B { public C() : base() { Console.WriteLine("C"); } }
Voir la réponseMasquer la réponse de l'exercice 4
A
B
C
Le constructeur de C appelle d'abord celui de B (: base(), qui serait de toute façon implicite), qui appelle d'abord celui de A. Les corps s'exécutent donc de la classe la plus générale à la plus spécialisée.
Tu avais trouvé ?