Une classe abstraite est une classe qui n'existe que pour qu'on en dérive. Elle ne peut pas être instanciée, et elle peut déclarer des membres abstraits : des méthodes et propriétés sans corps que chaque classe dérivée concrète doit implémenter. Pour tout le reste, c'est une classe ordinaire : elle peut avoir des champs, des constructeurs et des méthodes entièrement implémentées dont héritent les classes dérivées.
Déclarer une classe abstraite
Marquez la classe abstract, et marquez abstract chaque membre que les classes dérivées doivent fournir, en le terminant par un point-virgule au lieu d'un corps :
Sortie :
Circle: area 7.07, perimeter 9.42
Rectangle: area 10.00, perimeter 13.00
Describe est écrite une fois, dans la classe de base, et appelle Area et Perimeter, qui n'existent pas du tout dans Shape. C'est tout l'intérêt d'un membre abstrait : la classe de base peut compter dessus, et le compilateur garantit que chaque forme concrète le fournit. Une classe dérivée qui en oublie un ne compile pas :
class Triangle : Shape
{
public Triangle() : base("Triangle") { }
public override double Area() => 6;
}
// error CS0534: 'Triangle' does not implement inherited abstract member 'Shape.Perimeter()'
La correction consiste à implémenter Perimeter, ou à marquer Triangle lui-même abstract s'il s'agit d'une base intermédiaire pour d'autres classes.
Règles des membres abstraits
- Un membre abstrait n'a pas de corps et est implicitement virtuel : les classes dérivées l'implémentent avec
override. - Seules les classes abstraites peuvent contenir des membres abstraits. En placer un dans une classe normale est une erreur de compilation.
- Les membres abstraits ne peuvent pas être
private(personne ne pourrait les redéfinir) nistaticdans une classe. - Les méthodes, propriétés, indexeurs et événements peuvent être abstraits. Les champs et les constructeurs non.
- Une classe abstraite n'est pas obligée de contenir un membre abstrait. Marquer une classe
abstractempêche simplement son instanciation, ce qui est utile pour une classe de base qui n'a de sens qu'à travers ses sous-classes.
Une propriété abstraite déclare les accesseurs que la classe dérivée doit fournir :
abstract class Plan
{
public abstract decimal MonthlyPrice { get; }
public abstract string Name { get; }
}
class ProPlan : Plan
{
public override decimal MonthlyPrice => 12.99m;
public override string Name => "Pro";
}
Constructeurs dans les classes abstraites
Une classe abstraite peut avoir des constructeurs. On ne peut pas les appeler avec new, mais chaque constructeur dérivé en exécute un via : base(...), ils sont donc l'endroit où l'état partagé est initialisé et validé. Déclarez-les protected, ce qui dit exactement cela : seules les classes dérivées peuvent l'utiliser.
Le patron de méthode
La raison la plus courante d'écrire une classe abstraite est de fixer l'ordre d'un algorithme dans la classe de base et de laisser les classes dérivées remplir chaque étape. La méthode de base qui exécute les étapes n'est pas virtuelle, donc aucune sous-classe ne peut les réordonner ou en sauter.
Sortie :
item,qty,price
Coffee beans,2,11.50
Filter papers,1,3.20
| Item | Qty | Price |
| --- | --- | --- |
| Coffee beans | 2 | 11.50 |
| Filter papers | 1 | 3.20 |
Total: 26.20
Chaque ligne est un tuple nommé, et la sortie est construite avec un StringBuilder. Remarquez le mélange : Header et Row sont abstract parce que chaque format doit les définir, et Footer est virtual avec un comportement par défaut vide parce que la plupart des formats n'ont besoin de rien à cet endroit. Les étapes sont protected, donc le code extérieur ne peut appeler que Export. Les classes du framework utilisent la même idée : Stream et TextWriter sont des classes de base abstraites dont les nombreuses méthodes pratiques reposent sur quelques membres fournis par une classe dérivée (Read et Write pour un flux, Write(char) pour un writer de texte).
Classe abstraite ou interface
Les deux permettent d'écrire du code pour un type sans connaître la classe concrète. Elles diffèrent par ce qu'elles peuvent contenir et par le nombre qu'une classe peut en avoir :
| Classe abstraite | Interface | |
|---|---|---|
| Une classe peut en hériter ou en implémenter | une | plusieurs |
| Champs d'instance (état) | oui | non |
| Constructeurs | oui | non |
| Méthodes avec un corps | oui | oui, depuis C# 8 (méthodes d'interface par défaut) |
| Membres statiques | oui | oui, depuis C# 8 ; membres statiques abstraits depuis C# 11 |
| Modificateurs d'accès sur les membres | tous | public par défaut ; autres autorisés depuis C# 8 |
| Utilisable par les structs | non | oui |
| Ajouter un membre plus tard | sans risque s'il n'est pas abstrait | casse les implémentations, sauf s'il a un corps par défaut |
Les méthodes d'interface par défaut (C# 8) ont réduit l'écart, mais pas la différence de fond :
public interface ILogger
{
void Write(string message);
void Error(string message) => Write("ERROR: " + message); // default implementation: C# 8, .NET Core 3.0+
}
Une méthode par défaut ne peut toujours pas toucher aux champs d'instance, puisque l'interface n'en a pas ; elle ne peut qu'appeler d'autres membres de l'interface.
Une façon pratique de choisir :
- Une interface quand vous décrivez une capacité que des types sans rapport peuvent avoir (
IComparable<T>,IDisposable,IShippingProvider), quand des structs doivent pouvoir y participer, ou quand une classe a besoin de plusieurs rôles de ce genre. La plupart du code d'injection de dépendances est écrit pour des interfaces pour cette raison. - Une classe abstraite quand les types dérivés sont vraiment des variantes d'une même chose et partagent un état ou un algorithme fixe, comme les exportateurs ci-dessus.
- Les deux est courant : une interface dont dépendent les appelants, et une classe de base abstraite qui l'implémente et évite aux implémenteurs de réécrire les parties communes. La page sur les interfaces couvre en détail le côté interface.
Erreurs courantes
- Essayer d'instancier la classe de base.
new Shape(...)donne CS0144. Instanciez une classe dérivée. - Oublier
overridesur l'implémentation. Écrirepublic double Area()dans la classe dérivée n'implémente pas le membre abstrait ; le compilateur signale CS0534 pour l'implémentation manquante, plus un avertissement indiquant que la nouvelle méthode masque celle héritée. - Un constructeur public sur une classe abstraite. Sans danger, mais trompeur. Utilisez
protected. - Des classes abstraites sans code partagé. Si la classe de base n'a que des membres abstraits et aucun état, une interface dit la même chose et n'occupe pas l'unique place de classe de base.
- Appeler des membres abstraits depuis le constructeur de base. L'implémentation dérivée s'exécute avant le corps du constructeur dérivé, donc les champs que ce corps affecte valent encore
0ounull.
Questions fréquentes
Qu'est-ce qu'une classe abstraite en C# ?
Une classe marquée abstract ne sert que de classe de base : vous ne pouvez pas en créer d'instance avec new (erreur CS0144). Elle peut contenir des champs, des constructeurs et des méthodes normaux, plus des membres abstract sans corps que chaque classe dérivée non abstraite doit implémenter.
Quelle est la différence entre une classe abstraite et une interface en C# ?
Une classe ne peut dériver que d'une seule classe abstraite mais peut implémenter plusieurs interfaces. Une classe abstraite peut détenir un état (champs d'instance), des constructeurs et des membres de n'importe quel niveau d'accès, et c'est l'endroit naturel pour une implémentation partagée. Une interface décrit une capacité sans état d'instance ; depuis C# 8 elle peut inclure des corps de méthode par défaut, mais elle ne peut toujours pas avoir de champs d'instance ni de constructeurs.
Quelle est la différence entre les méthodes abstract et virtual en C# ?
Une méthode virtual a un corps que les classes dérivées peuvent redéfinir. Une méthode abstract n'a pas de corps et les classes dérivées doivent la redéfinir, sinon elles sont elles-mêmes abstraites (erreur CS0534 pour une classe concrète). Les méthodes abstraites ne peuvent apparaître que dans des classes abstraites.
Une classe abstraite peut-elle avoir un constructeur en C# ?
Oui. Il s'exécute quand une classe dérivée est construite, via : base(...), et initialise en général les champs partagés. Déclarez-le protected : un constructeur public sur une classe abstraite laisse croire que les appelants peuvent l'utiliser, alors qu'ils ne le peuvent pas.
Pourquoi ne peut-on pas créer d'instance d'une classe abstraite ?
Parce qu'elle peut avoir des membres abstraits sans implémentation : appeler new Shape().Area() n'aurait aucun code à exécuter. Le compilateur signale CS0144 (« Cannot create an instance of the abstract type or interface »). Créez plutôt une instance d'une classe dérivée concrète, et gardez-la dans une variable du type abstrait si vous le souhaitez : Shape s = new Circle(2);.