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
virtualetoverride; - 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
switchsur 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.
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)
{
}
}
virtualdans la classe mère autorise les classes filles à redéfinir le membre (une méthode ou, comme ici, une propriété).overridedans la classe fille le redéfinit : même nom, même signature, nouveau code.Imprimantene redéfinit rien : elle garde la durée par défaut de 5 ans.ValeurNetten'est écrite qu'une fois, dansMateriel, et litDureeAmortissementAns: elle utilisera automatiquement la bonne durée pour chaque objet.
La liaison dynamique
Le code qui parcourt le parc ne connaît que Materiel :
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}");
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.
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) » :
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).
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 écris | Ce qui se passe |
|---|---|
override dans la fille, mais pas de virtual (ni abstract) dans la mère | erreur 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 override | avertissement CS0108 : la méthode masque celle de la mère ; ajoute new si c'est voulu |
new dans la fille | masquage : 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 :
// dans Materiel
public string Categorie() => "matériel";
// dans Portable
public new string Categorie() => "portable"; // masque, ne redéfinit pas
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 |
| Nom | identique | identique |
| Paramètres | différents (nombre ou types) | identiques |
| Mot-clé | aucun | virtual 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 savant | polymorphisme 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 :
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));
}
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 :
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.
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";
}
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
[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 :
PosteFixegarde le calcul par défaut ;PortableProajoute 60 € d'assurance au calcul par défaut ;ImprimanteLasera 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
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.
- Dans
Ticket:void Resoudre()etvoid Resoudre(DateTime date). virtual string Decrire()dansMateriel,override string Decrire()dansPoste.int Total()etdecimal Total()dans la même classe.string Decrire()(sansvirtual) dansMateriel,override string Decrire()dansPoste.virtual string Decrire()dansMateriel,string Decrire()(sans mot-clé) dansPoste.
Voir la réponseMasquer la réponse de l'exercice 3
- Surcharge : même nom, paramètres différents, dans la même classe.
- Redéfinition : même signature,
virtualdans la mère,overridedans la fille. - 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).
- Erreur de compilation CS0506 : on ne peut pas redéfinir une méthode qui n'est ni
virtual, niabstract, nioverride. - Masquage avec l'avertissement CS0114 : la méthode de
Postecache celle deMaterielau lieu de la redéfinir. Le compilateur conseille d'ajouteroverride(pour redéfinir) ounew(pour confirmer le masquage) ; ici, c'estoverridequ'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.
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 :
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é ?