Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

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, as et switch ;
  • 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.

Materiel+ NumInventaire : string+ Marque : string+ Modele : string+ PrixAchat : decimal# EnReparation : bool+ Etiquette() : stringPoste+ Utilisateur : string?Imprimante+ Couleur : bool«sealed»Portable+ Chiffre : bool+ PeutSortirDuSite() : boolest unest un
Une hiérarchie sur trois niveaux : Portable hérite de Poste, qui hérite de Materiel
VocabulaireSynonymes
Classe mèreclasse de base, classe parente, super-classe, classe générale
Classe filleclasse dérivée, sous-classe, classe spécialisée
Hériter dedériver de, spécialiser, étendre (extends en Java et en PHP)
Relation d'héritage en UMLgénéralisation : flèche à triangle vide, de la fille vers la mère

Écrire une classe dérivée

csharp
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;
}
csharp
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());
text
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 :

text
  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èreHérité ?Utilisable dans la fille ?Utilisable de l'extérieur ?
publicouiouioui
protectedouiouinon
privateoui (il existe dans l'objet)nonnon
constructeurnonseulement via base(...)via new sur la mère
staticaccessible par le nom de la classeoui, 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.

csharp
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.

csharp
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
ÉcritureSi le type ne correspond pas
m is Posterenvoie false
m is Poste prenvoie false, p n'est pas utilisable
m as Posterenvoie null
(Poste)mlève InvalidCastException

Le switch sait aussi filtrer sur le type, et même sur des propriétés :

csharp
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}");
}
text
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'objectRôleRedéfinie au
ToString()représentation texte ; par défaut, le nom complet du typechapitre 8
Equals(object?)égalité ; par défaut, des référenceschapitre 11
GetHashCode()code de hachage, utilisé par Dictionary et HashSetchapitre 11
GetType()le type réel de l'objet
csharp
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.

  1. « 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.
  2. Tout ce que fait la mère a-t-il du sens pour la fille ? Si Imprimante héritait de Poste pour récupérer Etiquette(), elle hériterait aussi d'Utilisateur et de l'installation de logiciels, absurdes pour une imprimante. C'est le principe de substitution de Liskov (chapitre 12).
SituationChoix
Les classes partagent une vraie nature (« est un ») et des règles communeshéritage
Une classe veut seulement réutiliser une fonction d'une autrecomposition : 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
csharp
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.

csharp
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
  1. Le constructeur d'Imprimante n'appelle pas base(numInventaire, prix). C# cherche un constructeur sans paramètre dans Materiel, qui n'en a pas : erreur CS7036. Correction : public Imprimante(string numInventaire, decimal prix, bool couleur) : base(numInventaire, prix).
  2. CoutAnnuel() lit prixAchat, un champ private de Materiel : erreur CS0122, inaccessible en raison de son niveau de protection. Correction : exposer une propriété public decimal PrixAchat { get; } (ou protected) dans Materiel et l'utiliser.
  3. Scanner : Imprimante, Materiel : une classe ne peut avoir qu'une classe mère. Ici, c'est même inutile, puisqu'Imprimante hérite déjà de Materiel. Correction : class Scanner : Imprimante, avec son propre constructeur qui appelle base(...).

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.

  1. Administrateur et Utilisateur d'une application.
  2. Ticket et Intervention.
  3. Serveur et Materiel.
  4. CompteBancaire et Client.
  5. Stagiaire et Employe, sachant qu'un stagiaire peut être embauché à la fin de son stage.
Voir la réponseMasquer la réponse de l'exercice 3
  1. 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.
  2. Composition : un ticket a des interventions.
  3. Héritage : un serveur est un matériel, avec un numéro d'inventaire, un prix et un amortissement.
  4. Association : un client a un ou plusieurs comptes ; un compte n'est pas une sorte de client.
  5. Pas d'héritage figé : un objet ne change pas de classe. Si le stagiaire est embauché, il faudrait détruire l'objet Stagiaire et en créer un autre. Mieux vaut une classe Employe avec une propriété TypeContrat (stage, CDD, CDI), ou une association vers un objet Contrat.

Tu avais trouvé ?

Exercice 4 sur 4 Ordre des constructeurs Réussi À revoir

Qu'affiche new C(); ?

csharp
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
text
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é ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Polymorphisme : virtual, override et surchargeRevoir le sommaire du cours