C# : Les classes abstraites
Classes abstraites, méthodes abstraites vs virtuelles, sealed - quand et comment utiliser l'abstraction en C#.
C# : Les classes abstraites
Dans l'article sur les objets, on a vu l'héritage avec virtual et override. Mais parfois, une classe parent n'a pas de sens toute seule - elle existe uniquement pour être héritée. C'est le rôle des classes abstraites.
# Qu'est-ce qu'une classe abstraite ?
Une classe abstraite est une classe qu'on ne peut pas instancier directement. Elle sert de base commune pour d'autres classes, en fournissant du code partagé et en imposant certaines méthodes que les enfants doivent implémenter.
abstract class Forme
{
public string Couleur { get; set; }
// Méthode abstraite : pas de corps, les enfants DOIVENT l'implémenter
public abstract double CalculerAire();
// Méthode concrète : du code partagé par tous les enfants
public void Afficher()
{
Console.WriteLine($"Forme {Couleur}, aire = {CalculerAire()}");
}
}abstract class Forme{ public string Couleur { get; set; } // Méthode abstraite : pas de corps, les enfants DOIVENT l'implémenter public abstract double CalculerAire(); // Méthode concrète : du code partagé par tous les enfants public void Afficher() { Console.WriteLine($"Forme {Couleur}, aire = {CalculerAire()}"); }}Forme f = new Forme(); // Erreur de compilation ! Impossible d'instancier une classe abstraiteForme f = new Forme(); // Erreur de compilation ! Impossible d'instancier une classe abstraite# Implémenter une classe abstraite
Les classes enfants héritent du code concret et doivent implémenter les méthodes abstraites :
class Cercle : Forme
{
public double Rayon { get; }
public Cercle(double rayon, string couleur)
{
Rayon = rayon;
Couleur = couleur;
}
public override double CalculerAire()
{
return Math.PI * Rayon * Rayon;
}
}
class Rectangle : Forme
{
public double Largeur { get; }
public double Hauteur { get; }
public Rectangle(double largeur, double hauteur, string couleur)
{
Largeur = largeur;
Hauteur = hauteur;
Couleur = couleur;
}
public override double CalculerAire()
{
return Largeur * Hauteur;
}
}class Cercle : Forme{ public double Rayon { get; } public Cercle(double rayon, string couleur) { Rayon = rayon; Couleur = couleur; } public override double CalculerAire() { return Math.PI * Rayon * Rayon; }} class Rectangle : Forme{ public double Largeur { get; } public double Hauteur { get; } public Rectangle(double largeur, double hauteur, string couleur) { Largeur = largeur; Hauteur = hauteur; Couleur = couleur; } public override double CalculerAire() { return Largeur * Hauteur; }}Les deux classes héritent de Afficher() gratuitement, mais chacune fournit sa propre implémentation de CalculerAire(). Si un enfant oublie d'implémenter une méthode abstraite, le compilateur refuse de compiler.
Cercle c = new Cercle(5, "Rouge");
c.Afficher(); // "Forme Rouge, aire = 78.54..."
Rectangle r = new Rectangle(4, 3, "Bleu");
r.Afficher(); // "Forme Bleu, aire = 12"Cercle c = new Cercle(5, "Rouge");c.Afficher(); // "Forme Rouge, aire = 78.54..." Rectangle r = new Rectangle(4, 3, "Bleu");r.Afficher(); // "Forme Bleu, aire = 12"# abstract vs virtual
Les deux permettent aux enfants de redéfinir un comportement, mais avec une différence clé :
abstract | virtual | |
|---|---|---|
| Corps dans le parent | Non (interdit) | Oui (obligatoire) |
| L'enfant doit redéfinir | Oui (obligatoire) | Non (optionnel) |
| Le parent peut être instancié | Non | Oui |
abstract class Animal
{
// Abstrait : chaque animal DOIT définir son cri
public abstract string Crier();
// Virtuel : comportement par défaut, redéfinissable
public virtual string Decrire()
{
return $"Je suis un animal qui fait : {Crier()}";
}
}
class Chat : Animal
{
public override string Crier() => "Miaou";
// Decrire() est hérité tel quel - pas obligé de le redéfinir
}
class Chien : Animal
{
public override string Crier() => "Wouf";
public override string Decrire()
{
return $"Je suis un bon chien ! {Crier()} !";
}
}abstract class Animal{ // Abstrait : chaque animal DOIT définir son cri public abstract string Crier(); // Virtuel : comportement par défaut, redéfinissable public virtual string Decrire() { return $"Je suis un animal qui fait : {Crier()}"; }} class Chat : Animal{ public override string Crier() => "Miaou"; // Decrire() est hérité tel quel - pas obligé de le redéfinir} class Chien : Animal{ public override string Crier() => "Wouf"; public override string Decrire() { return $"Je suis un bon chien ! {Crier()} !"; }}En résumé : abstract = "tu n'as pas le choix, implémente-le". virtual = "j'ai une version par défaut, mais tu peux la changer".
# Le mot-clé sealed
sealed est l'opposé de abstract : il interdit l'héritage. Une classe sealed ne peut pas avoir d'enfants.
sealed class Configuration
{
public string ConnectionString { get; init; }
public int Timeout { get; init; }
}
// class MaConfig : Configuration { } // Erreur ! Configuration est sealedsealed class Configuration{ public string ConnectionString { get; init; } public int Timeout { get; init; }} // class MaConfig : Configuration { } // Erreur ! Configuration est sealedOn peut aussi seal une méthode spécifique dans une chaîne d'héritage pour empêcher les enfants de la redéfinir plus loin :
class Animal
{
public virtual string Crier() => "...";
}
class Chat : Animal
{
public sealed override string Crier() => "Miaou";
}
class ChatSauvage : Chat
{
// public override string Crier() => "MIAOU !!!"; // Erreur ! Crier est sealed dans Chat
}class Animal{ public virtual string Crier() => "...";} class Chat : Animal{ public sealed override string Crier() => "Miaou";} class ChatSauvage : Chat{ // public override string Crier() => "MIAOU !!!"; // Erreur ! Crier est sealed dans Chat}Quand utiliser sealed ?
- Quand une classe n'est pas conçue pour être héritée (la majorité des classes)
- Pour des raisons de performance : le compilateur peut optimiser les appels de méthodes sur les classes sealed (pas de vtable lookup)
- Par sécurité : empêcher un code tiers de modifier le comportement en héritant
En pratique, beaucoup de développeurs C# recommandent de mettre sealed par défaut sur toutes les classes et de ne l'enlever que quand l'héritage est explicitement voulu. C'est le même principe que private par défaut : on ferme d'abord, on ouvre si besoin.
# Classe abstraite vs interface
C'est la question classique. Le tableau complet est dans l'article sur les interfaces, mais voici la règle simple :
- Classe abstraite -> tu veux partager du code entre les enfants (champs, constructeurs, méthodes concrètes)
- Interface -> tu veux définir un contrat que des classes sans lien de parenté peuvent respecter
Et on peut combiner les deux :
interface IAffichable
{
void Afficher();
}
abstract class Forme : IAffichable
{
public string Couleur { get; set; }
public abstract double CalculerAire();
public void Afficher()
{
Console.WriteLine($"{Couleur}, aire = {CalculerAire()}");
}
}interface IAffichable{ void Afficher();} abstract class Forme : IAffichable{ public string Couleur { get; set; } public abstract double CalculerAire(); public void Afficher() { Console.WriteLine($"{Couleur}, aire = {CalculerAire()}"); }}Forme est abstraite (on ne peut pas l'instancier) ET implémente IAffichable (elle respecte le contrat). Les enfants héritent les deux.
# La suite
- Les interfaces - contrats et polymorphisme
- Les structs - value types personnalisés
- Les records - immutabilité et comparaison par valeur
- Les enums - énumérations et constantes nommées