Aller au contenu
Réussir mon BTS SIO

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

Par exemple

Chapitre 821 minIntermédiaire

Polymorphisme : virtual, override et surcharge

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • redéfinir une méthode avec virtual et override ;
  • prévoir quelle version d'une méthode s'exécute ;
  • éviter le piège du masquage avec new ;
  • distinguer surcharge et redéfinition ;
  • remplacer un switch sur le type par du polymorphisme.

4 exercices corrigés1 schéma

Le polymorphisme est la capacité d'un même appel de méthode à produire un comportement différent selon le type réel de l'objet qui le reçoit. En C#, la classe mère déclare une méthode virtual, chaque classe fille la redéfinit avec override, et c'est l'objet réel, pas le type de la variable, qui décide du code exécuté. Ce chapitre explique la redéfinition et la liaison dynamique, la différence avec la surcharge, les pièges de new et l'oubli de virtual, et comment le polymorphisme remplace les cascades de if sur le type.

Redéfinir une méthode : virtual et override

Sorbier Logistique amortit son matériel sur une durée qui dépend du type : 5 ans par défaut, 4 ans pour un poste fixe, 3 ans pour un portable. La valeur nette comptable se calcule de la même façon pour tous (amortissement linéaire par années complètes), seule la durée change.

csharp
class Materiel
{
    public Materiel(string numInventaire, string marque, string modele, DateOnly dateAchat, decimal prixAchat)
    {
        NumInventaire = numInventaire;
        Marque = marque;
        Modele = modele;
        DateAchat = dateAchat;
        PrixAchat = prixAchat;
    }

    public string NumInventaire { get; }
    public string Marque { get; }
    public string Modele { get; }
    public DateOnly DateAchat { get; }
    public decimal PrixAchat { get; }

    // Durée par défaut, que les classes filles peuvent redéfinir
    public virtual int DureeAmortissementAns => 5;

    public int AgeAns(DateOnly date)
    {
        int age = date.Year - DateAchat.Year;
        if (date < DateAchat.AddYears(age))
        {
            age--;
        }
        return age;
    }

    // Amortissement linéaire par années complètes
    public decimal ValeurNette(DateOnly date)
    {
        int age = AgeAns(date);
        if (age >= DureeAmortissementAns)
        {
            return 0m;
        }
        return Math.Round(PrixAchat * (DureeAmortissementAns - age) / DureeAmortissementAns, 2);
    }

    public override string ToString() => $"{NumInventaire} {Marque} {Modele}";
}

class Poste : Materiel
{
    public Poste(string numInventaire, string marque, string modele, DateOnly dateAchat, decimal prixAchat)
        : base(numInventaire, marque, modele, dateAchat, prixAchat)
    {
    }

    public override int DureeAmortissementAns => 4;
}

class Portable : Poste
{
    public Portable(string numInventaire, string marque, string modele, DateOnly dateAchat, decimal prixAchat)
        : base(numInventaire, marque, modele, dateAchat, prixAchat)
    {
    }

    public override int DureeAmortissementAns => 3;

    public override string ToString() => base.ToString() + " (portable)";
}

class Imprimante : Materiel
{
    public Imprimante(string numInventaire, string marque, string modele, DateOnly dateAchat, decimal prixAchat)
        : base(numInventaire, marque, modele, dateAchat, prixAchat)
    {
    }
}
  • virtual dans la classe mère autorise les classes filles à redéfinir le membre (une méthode ou, comme ici, une propriété).
  • override dans la classe fille le redéfinit : même nom, même signature, nouveau code.
  • Imprimante ne redéfinit rien : elle garde la durée par défaut de 5 ans.
  • ValeurNette n'est écrite qu'une fois, dans Materiel, et lit DureeAmortissementAns : elle utilisera automatiquement la bonne durée pour chaque objet.

La liaison dynamique

Le code qui parcourt le parc ne connaît que Materiel :

csharp
DateOnly aujourdhui = new DateOnly(2026, 11, 2);
List<Materiel> parc =
[
    new Poste("PC-0001", "Dell", "OptiPlex 7020", new DateOnly(2024, 9, 2), 899m),
    new Portable("PC-0002", "Lenovo", "ThinkPad T14", new DateOnly(2024, 1, 10), 1290m),
    new Imprimante("IM-0001", "Brother", "HL-L8360", new DateOnly(2022, 6, 20), 529m),
    new Portable("PC-0003", "HP", "EliteBook 840", new DateOnly(2022, 3, 1), 1149m),
];

foreach (Materiel m in parc)
{
    Console.WriteLine($"{m} | amorti sur {m.DureeAmortissementAns} ans | valeur nette : {m.ValeurNette(aujourdhui):C}");
}
decimal total = parc.Sum(m => m.ValeurNette(aujourdhui));
Console.WriteLine($"Valeur nette du parc : {total:C}");
text
PC-0001 Dell OptiPlex 7020 | amorti sur 4 ans | valeur nette : 449,50 €
PC-0002 Lenovo ThinkPad T14 (portable) | amorti sur 3 ans | valeur nette : 430,00 €
IM-0001 Brother HL-L8360 | amorti sur 5 ans | valeur nette : 105,80 €
PC-0003 HP EliteBook 840 (portable) | amorti sur 3 ans | valeur nette : 0,00 €
Valeur nette du parc : 985,30 €

Vérifie le premier calcul : acheté le 2 septembre 2024, le poste a 2 ans complets le 2 novembre 2026 ; il lui reste 2 ans sur 4, soit 899 × 2 / 4 = 449,50 €. Le portable PC-0003, âgé de 4 ans pour une durée de 3, est totalement amorti.

Dans la boucle, m est déclaré Materiel. Pourtant, m.DureeAmortissementAns renvoie 4, 3 ou 5 selon l'objet. C'est la liaison dynamique (late binding) : pour un membre virtual, le choix du code à exécuter se fait à l'exécution, d'après le type réel de l'objet, et non à la compilation d'après le type de la variable.

un seul appelobjets de la liste parccode exécutéforeach (Materiel m in parc)m.DureeAmortissementAnsPC-0001un PostePC-0002un PortableIM-0001une ImprimantePC-0003un PortableMaterielvirtual=> 5Posteoverride=> 4Portableoverride=> 3l'objet réel choisit la version, pas le type de la variable m
Un seul appel, trois comportements : l'objet réel choisit l'implémentation

C'est ce qui rend le code ouvert à l'extension : ajouter demain une classe Serveur avec sa propre durée ne demande aucune modification de la boucle ni de ValeurNette.

base.Methode() : compléter au lieu de remplacer

Une redéfinition peut appeler la version de la classe mère avec base., puis la compléter. Le ToString() de Portable réutilise celui de Materiel et ajoute « (portable) » :

csharp
public override string ToString() => base.ToString() + " (portable)";

C'est la bonne pratique dès que la fille ajoute quelque chose au comportement de la mère : le code commun reste à un seul endroit.

Redéfinir ToString()

ToString() est une méthode virtual héritée d'object. Par défaut, elle renvoie le nom complet du type (GestParc.Console.Poste), ce qui n'aide personne. La redéfinir donne une représentation lisible, utilisée automatiquement par Console.WriteLine(objet), par l'interpolation $"{objet}", par le débogueur et par les listes déroulantes de WinForms (les contrôles WinForms).

csharp
public override string ToString() => $"{NumInventaire} {Marque} {Modele}";

override, new et l'oubli de virtual

Trois erreurs classiques autour de la redéfinition :

Ce que tu écrisCe qui se passe
override dans la fille, mais pas de virtual (ni abstract) dans la mèreerreur CS0506 : « impossible de substituer le membre hérité, car il n'est pas marqué comme virtual, abstract ou override »
Une méthode de même signature dans la fille, sans overrideavertissement CS0108 : la méthode masque celle de la mère ; ajoute new si c'est voulu
new dans la fillemasquage : la version appelée dépend du type de la variable, pas de l'objet

Le masquage avec new est un piège, parce qu'il casse le polymorphisme sans erreur :

csharp
// dans Materiel
public string Categorie() => "matériel";

// dans Portable
public new string Categorie() => "portable";       // masque, ne redéfinit pas
csharp
Portable pt = new Portable("PC-0004", "Dell", "Latitude 5450", new DateOnly(2026, 3, 30), 1049.90m);
Materiel vueMere = pt;
Console.WriteLine(pt.Categorie());        // portable
Console.WriteLine(vueMere.Categorie());   // matériel : le même objet répond autre chose !

Le même objet donne deux réponses selon la variable par laquelle on l'atteint. Avec virtual dans la mère et override dans la fille, les deux lignes afficheraient « portable ».

Une fille peut aussi bloquer la suite de la chaîne avec sealed override : ses propres classes filles ne pourront plus redéfinir la méthode.

Surcharge et redéfinition : ne pas confondre

Les deux mots reviennent dans tous les sujets, et la confusion coûte des points.

Surcharge (overloading)Redéfinition (overriding)
Où ?dans la même classe (ou entre mère et fille)dans une classe fille
Nomidentiqueidentique
Paramètresdifférents (nombre ou types)identiques
Mot-cléaucunvirtual dans la mère, override dans la fille
Choix de la versionà la compilation, d'après les argumentsà l'exécution, d'après l'objet réel
Nom savantpolymorphisme ad hoc (ou statique)polymorphisme d'héritage (ou dynamique)

Un exemple de surcharge, avec quatre méthodes Prix qui ne diffèrent que par leurs paramètres :

csharp
static class Formateur
{
    public static string Prix(decimal montant) => $"{montant:C}";
    public static string Prix(decimal montant, bool avecTva) => avecTva ? $"{montant * 1.2m:C} TTC" : Prix(montant);
    public static string Prix(int euros) => $"{euros} €";
    public static string Prix(string saisie) => Prix(decimal.Parse(saisie));
}
csharp
Console.WriteLine(Formateur.Prix(1049.9m));                  // 1 049,90 €
Console.WriteLine(Formateur.Prix(1049.9m, avecTva: true));   // 1 259,88 € TTC
Console.WriteLine(Formateur.Prix(12));                       // 12 €
Console.WriteLine(Formateur.Prix("1049,90"));                // 1 049,90 €

Le compilateur choisit la version d'après les arguments écrits dans l'appel. Le type de retour ne compte pas : deux méthodes qui ne diffèrent que par lui sont refusées. Les constructeurs surchargés du chapitre 4 sont un autre exemple de surcharge.

Remplacer un switch sur le type par le polymorphisme

Sans polymorphisme, on écrit ce genre de code :

csharp
int Duree(Materiel m)
{
    if (m is Portable) return 3;
    if (m is Poste) return 4;
    return 5;
}

Chaque nouveau type de matériel oblige à retrouver et à modifier toutes ces fonctions, dispersées dans l'application. Avec le polymorphisme, la connaissance est rangée dans la classe concernée : ajouter un type, c'est ajouter une classe. C'est le principe ouvert/fermé du chapitre 12.

La règle pratique : si tu écris un if ou un switch sur le type d'un objet pour choisir un comportement, demande-toi si ce comportement ne devrait pas être une méthode virtuelle de l'objet lui-même.

Exercices

Exercice 1 sur 4 Prédire les affichages Réussi À revoir

Donne les six lignes affichées.

csharp
class Notification(string message)
{
    public string Message { get; } = message;
    public virtual string Texte() => $"[info] {Message}";
    public string Canal() => "courriel";
    public override string ToString() => Texte();
}

class NotificationUrgente(string message) : Notification(message)
{
    public override string Texte() => base.Texte().Replace("[info]", "[URGENT]") + " !";
    public new string Canal() => "SMS";
}
csharp
Notification n1 = new Notification("Ticket 4 créé");
Notification n2 = new NotificationUrgente("Serveur inaccessible");
NotificationUrgente n3 = new NotificationUrgente("Wi-Fi coupé");
Console.WriteLine(n1.Texte());
Console.WriteLine(n2.Texte());
Console.WriteLine(n3.Texte());
Console.WriteLine(n2.Canal());
Console.WriteLine(n3.Canal());
Console.WriteLine(n2);
Voir la réponseMasquer la réponse de l'exercice 1
text
[info] Ticket 4 créé
[URGENT] Serveur inaccessible !
[URGENT] Wi-Fi coupé !
courriel
SMS
[URGENT] Serveur inaccessible !

Texte() est virtuelle : n2, bien que déclaré Notification, exécute la version de NotificationUrgente, qui reprend base.Texte() et remplace le préfixe. Canal() n'est pas virtuelle et la fille la masque avec new : par la variable n2 (type Notification), on obtient « courriel » ; par n3 (type NotificationUrgente), « SMS ». C'est exactement le piège du masquage. La dernière ligne appelle ToString(), redéfinie dans Notification, qui appelle Texte(), elle-même résolue sur l'objet réel.

Tu avais trouvé ?

Exercice 2 sur 4 Coût de maintenance polymorphe Réussi À revoir

Écris une classe Equipement (prix en lecture seule) avec une méthode virtuelle CoutMaintenanceAnnuel() qui renvoie 10 % du prix, arrondi au centime. Puis :

  • PosteFixe garde le calcul par défaut ;
  • PortablePro ajoute 60 € d'assurance au calcul par défaut ;
  • ImprimanteLaser a un nombre de pages par an et coûte 0,012 € par page, sans le forfait de 10 %.

Calcule le coût total pour un poste fixe à 899 €, un portable à 1 290 € et une imprimante à 529 € qui imprime 18 000 pages par an.

Voir la réponseMasquer la réponse de l'exercice 2
csharp
class Equipement(decimal prix)
{
    public decimal Prix { get; } = prix;
    public virtual decimal CoutMaintenanceAnnuel() => Math.Round(Prix * 0.10m, 2);
}

class PosteFixe(decimal prix) : Equipement(prix) { }

class PortablePro(decimal prix) : Equipement(prix)
{
    public override decimal CoutMaintenanceAnnuel() => base.CoutMaintenanceAnnuel() + 60m;
}

class ImprimanteLaser(decimal prix, int pagesParAn) : Equipement(prix)
{
    public int PagesParAn { get; } = pagesParAn;
    public override decimal CoutMaintenanceAnnuel() => Math.Round(PagesParAn * 0.012m, 2);
}
  • Poste fixe : 899 × 10 % = 89,90 € ;
  • portable : 1 290 × 10 % + 60 = 189,00 € (base. évite de recopier le calcul de 10 %) ;
  • imprimante : 18 000 × 0,012 = 216,00 €.

Total : eqs.Sum(e => e.CoutMaintenanceAnnuel()) = 494,90 €. Amélioration : le mot-clé abstract sur la classe (abstract class Equipement) empêcherait de créer un « équipement » sans type, comme le détaille le chapitre suivant.

Tu avais trouvé ?

Exercice 3 sur 4 Surcharge ou redéfinition ? Réussi À revoir

Pour chaque paire de méthodes, dis s'il s'agit d'une surcharge, d'une redéfinition, d'un masquage ou d'une erreur de compilation.

  1. Dans Ticket : void Resoudre() et void Resoudre(DateTime date).
  2. virtual string Decrire() dans Materiel, override string Decrire() dans Poste.
  3. int Total() et decimal Total() dans la même classe.
  4. string Decrire() (sans virtual) dans Materiel, override string Decrire() dans Poste.
  5. virtual string Decrire() dans Materiel, string Decrire() (sans mot-clé) dans Poste.
Voir la réponseMasquer la réponse de l'exercice 3
  1. Surcharge : même nom, paramètres différents, dans la même classe.
  2. Redéfinition : même signature, virtual dans la mère, override dans la fille.
  3. Erreur de compilation : deux méthodes ne peuvent pas différer seulement par leur type de retour (CS0111, le membre est déjà défini).
  4. Erreur de compilation CS0506 : on ne peut pas redéfinir une méthode qui n'est ni virtual, ni abstract, ni override.
  5. Masquage avec l'avertissement CS0114 : la méthode de Poste cache celle de Materiel au lieu de la redéfinir. Le compilateur conseille d'ajouter override (pour redéfinir) ou new (pour confirmer le masquage) ; ici, c'est override qu'il faut.

Tu avais trouvé ?

Exercice 4 sur 4 Supprimer le switch Réussi À revoir

Ce code calcule le délai de remplacement en cas de panne. Réécris-le avec le polymorphisme pour qu'un nouveau type de matériel n'oblige plus à modifier DelaiRemplacementJours.

csharp
static int DelaiRemplacementJours(Materiel m) => m switch
{
    Portable => 1,       // stock de portables de prêt
    Poste => 2,
    Imprimante => 5,
    _ => 10,
};
Voir la réponseMasquer la réponse de l'exercice 4

On déplace la connaissance dans les classes :

csharp
class Materiel
{
    public virtual int DelaiRemplacementJours => 10;
}

class Poste : Materiel
{
    public override int DelaiRemplacementJours => 2;
}

class Portable : Poste
{
    public override int DelaiRemplacementJours => 1;
}

class Imprimante : Materiel
{
    public override int DelaiRemplacementJours => 5;
}

L'appel devient m.DelaiRemplacementJours, quel que soit le matériel. Une future classe Serveur déclarera sa propre valeur, ou héritera des 10 jours par défaut, sans que personne ne touche au code existant. (Les constructeurs sont omis pour la lisibilité.)

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 : Classe abstraite et interface : différence et usagesRevoir le sommaire du cours