Menu

Héritage en C# : base, virtual, override, sealed et polymorphisme

Comment fonctionne l'héritage de classes en C# : dériver avec deux-points, ce qui est hérité et ce qui ne l'est pas, appeler la classe de base avec base, virtual et override, masquer avec new (et la sortie qui surprend tout le monde), sealed, le polymorphisme, et les conversions vers le haut et vers le bas d'une hiérarchie.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

L'héritage permet à une classe de s'appuyer sur une autre. La classe dérivée reçoit les membres de la classe de base, ajoute les siens et peut remplacer le comportement que la classe de base l'autorise à remplacer. Combiné aux méthodes virtual, il donne le polymorphisme : du code écrit pour la classe de base exécute le bon comportement dérivé sans savoir de quelle classe dérivée il s'agit.

Dériver une classe

Placez la classe de base après deux-points. La classe dérivée a tout ce qu'a la classe de base, plus ce qu'elle déclare :

Sortie :

TX-19: 20 km, 2 fares
True

Ce qu'une classe dérivée reçoit et ce qu'elle ne reçoit pas :

  • Hérités : les champs, propriétés, méthodes, événements et types imbriqués. Tout est physiquement dans l'objet.
  • Accessibles : seulement ce que la base autorise : les membres public, protected et internal. Un membre private de Vehicle existe dans chaque Taxi, mais le code de Taxi ne peut pas le nommer. C'est pourquoi Km a un private set : Taxi peut le lire, et ne le modifie que via Drive.
  • Non hérités : les constructeurs. Taxi doit déclarer les siens et chaîner vers l'un de ceux de Vehicle avec : base(plate). La page sur les constructeurs montre dans quel ordre les deux s'exécutent.

Toute classe dérive en fin de compte de object, c'est pourquoi chaque objet a ToString(), Equals() et GetHashCode().

virtual et override

Une classe de base marque une méthode virtual pour dire « les classes dérivées peuvent fournir leur propre version ». Une classe dérivée la remplace avec override. Dans la redéfinition, base.Method() appelle la version de la classe de base.

Sortie :

[email] to lea@example.com: Your order 1042 has shipped today
  (unsubscribe link appended)
[sms] to +351 912 000 111: Your order 1042 has...
[generic] to ops-team: Your order 1042 has shipped today

La variable de boucle est de type Notification, et pourtant chaque objet s'affiche à sa façon. C'est le polymorphisme : l'appel n.Render(...) est résolu à l'exécution d'après le type réel de l'objet. Remarquez aussi que base.Render dans Email utilise Channel, qui est lui-même virtuel, donc la méthode de base affiche email et non generic. Un appel virtuel dans la classe de base atteint quand même la redéfinition.

Les propriétés peuvent aussi être virtuelles, comme le montre Channel. Les champs non.

Le compilateur vous tient aux deux mots-clés. Écrire override sur une méthode qui n'est pas virtual donne l'erreur CS0506 (« cannot override inherited member ... because it is not marked virtual, abstract, or override »). Omettre override quand la méthode de base est virtuelle n'est qu'un avertissement, et cela change complètement le sens, comme le montre la section suivante.

new ou override : masquer au lieu de redéfinir

Si une classe dérivée déclare une méthode de même signature qu'une méthode de base sans écrire override, elle masque la méthode de base. Le compilateur avertit (CS0114 pour une méthode de base virtuelle, CS0108 sinon) et suggère le mot-clé new, qui fait taire l'avertissement mais conserve le comportement de masquage :

Sortie :

Sales report
Report
Draft report

b et c sont le même genre d'objet, et pourtant ils affichent des titres différents. Avec new, la méthode choisie dépend du type de la variable, décidé à la compilation. Le code qui manipule les rapports comme des Report (une liste, un paramètre de méthode, un callback de framework) ne voit jamais DraftReport.Title. Ce n'est presque jamais ce que vous voulez. Utilisez override pour le polymorphisme ; new existe surtout pour le cas où une classe de base que vous ne contrôlez pas ajoute un membre dont le nom entre en collision avec l'un des vôtres.

sealed

sealed sur une classe interdit d'en dériver :

sealed class Invoice { }
class CorrectedInvoice : Invoice { }
// error CS0509: 'CorrectedInvoice': cannot derive from sealed type 'Invoice'

string est sealed, comme de nombreux types du framework. Sur une redéfinition, sealed arrête la chaîne à ce niveau :

class Shape { public virtual string Name() => "shape"; }
class Square : Shape { public sealed override string Name() => "square"; }
class Tile : Square { public override string Name() => "tile"; }
// error CS0239: 'Tile.Name()': cannot override inherited member 'Square.Name()' because it is sealed

Concevoir une classe pour l'héritage demande du travail : décider ce qui est virtuel, ce sur quoi les classes dérivées peuvent compter, dans quel ordre les choses se passent. Une classe qui n'a pas été conçue ainsi est plus sûre scellée, et le scellement peut être levé plus tard sans casser personne, alors qu'on ne peut plus revenir sur un descellement une fois que d'autres dérivent de votre classe. Les appels aux membres de classes scellées peuvent aussi être légèrement plus rapides, car le runtime sait qu'aucune redéfinition n'existe.

Une classe de base, plusieurs interfaces

Une classe C# a exactement une classe de base. class Admin : User, Employee donne l'erreur CS1721 (« cannot have multiple base classes »). Une classe peut en revanche implémenter un nombre quelconque d'interfaces en plus de sa classe de base :

class Admin : User, IAuditable, IComparable<Admin>
{
    // base class first, then interfaces, in any order
}

Utilisez une classe de base pour « est une sorte de, et partage son implémentation avec », et des interfaces pour « sait faire ». Quand la classe de base n'existe que pour obliger les classes dérivées à fournir certaines méthodes, la classe abstraite est l'outil adapté.

Conversions vers le haut et vers le bas de la hiérarchie

Un objet dérivé peut toujours être utilisé là où son type de base est attendu. Cette conversion vers le haut (upcast) est implicite et ne peut pas échouer. Dans l'autre sens, une conversion vers le bas (downcast) demande un cast explicite et échoue à l'exécution si l'objet n'est pas de ce type :

Sortie :

Rex fetches the ball
InvalidCastException: Tom is not a Dog
True
Rex fetches the ball

Écrire Dog d = pet; sans le cast est une erreur de compilation (CS0266 : « An explicit conversion exists (are you missing a cast?) »), car le compilateur sait seulement que pet est un Animal. Préférez is avec une variable quand l'objet peut être d'un autre type, et un cast simple seulement quand toute autre chose serait un bug. Des downcasts fréquents sont un signe de mauvaise conception : cela signifie en général que le comportement appartient à une méthode virtuelle de la classe de base. La page sur le pattern matching couvre toutes les formes de is.

Héritage et collections

Le polymorphisme fonctionne élément par élément, mais les collections génériques d'un type dérivé ne sont pas des collections du type de base : List<Animal> animals = new List<Dog>(); ne compile pas, car la liste accepterait alors un Cat. Les vues en lecture seule sont covariantes, donc IEnumerable<Animal> animals = new List<Dog>(); convient. La page sur les génériques explique pourquoi.

Erreurs courantes

  • Oublier override. La méthode compile avec un avertissement et masque silencieusement au lieu de redéfinir. Traitez CS0114 comme une erreur.
  • Tout rendre virtual. Chaque membre virtuel est une promesse faite aux classes dérivées sur le moment où il est appelé et ce qu'il peut supposer. Ne marquez que les points d'extension voulus.
  • Des hiérarchies profondes. Trois ou quatre niveaux d'héritage rendent difficile de savoir quelle version d'une méthode s'exécute. La composition, une classe qui contient un autre objet et l'appelle, est souvent plus simple qu'un niveau de plus.
  • Appeler des méthodes virtuelles depuis un constructeur. La redéfinition s'exécute avant le corps du constructeur dérivé, donc tout champ que ce corps affecte contient encore sa valeur par défaut.
  • Utiliser l'héritage uniquement pour réutiliser du code. Si une Stack hérite de List, les appelants peuvent faire Insert au milieu de votre pile. Gardez plutôt une List dans un champ privé.

Questions fréquentes

Comment fonctionne l'héritage en C# ?

Une classe nomme une classe de base après deux-points : class Dog : Animal. La classe dérivée reçoit tous les membres de la classe de base (champs, propriétés, méthodes, événements), peut ajouter les siens et peut redéfinir ceux que la base a marqués virtual. Les constructeurs ne sont pas hérités, et les membres private, bien que présents dans l'objet, ne sont pas accessibles depuis la classe dérivée.

Quelle est la différence entre virtual et override en C# ?

virtual se place sur la méthode de la classe de base et signifie « les classes dérivées peuvent remplacer ceci ». override se place sur la méthode de la classe dérivée et effectue le remplacement. Les deux sont nécessaires : redéfinir une méthode qui n'est pas virtual, abstract ou déjà override donne l'erreur CS0506. Quand une méthode virtuelle est appelée, le runtime exécute la version du type réel de l'objet, pas celle du type de la variable.

Quelle est la différence entre new et override en C# ?

override remplace la méthode de base pour tous les appelants, même le code qui manipule l'objet via une variable de type base. new ne fait que la masquer : le code qui voit l'objet comme le type dérivé appelle la nouvelle méthode, tandis que le code qui le voit comme le type de base appelle toujours la méthode de base. Donc Base b = new Derived(); b.M(); exécute Derived.M avec override et Base.M avec new.

C# prend-il en charge l'héritage multiple ?

Pas pour les classes : une classe a exactement une classe de base, et en indiquer deux donne l'erreur CS1721. Une classe peut implémenter un nombre quelconque d'interfaces, et c'est ainsi que C# exprime « ce type sait faire plusieurs choses ». Depuis C# 8, les interfaces peuvent aussi porter des implémentations de méthodes par défaut.

Que signifie sealed en C# ?

Une classe sealed ne peut pas servir de classe de base ; en dériver donne l'erreur CS0509. string est sealed, par exemple. Sur une méthode, sealed override empêche les classes plus bas dans la hiérarchie de la redéfinir à nouveau (CS0239). Sceller les classes qui n'ont pas été conçues pour l'héritage est un choix par défaut raisonnable.

Comment appeler la méthode de la classe de base en C# ?

Utilisez base.MethodName(...) dans la classe dérivée, en général dans la redéfinition : public override string Describe() => base.Describe() + " with GPS";. Pour les constructeurs, utilisez : base(...) après la liste de paramètres pour choisir le constructeur de base qui s'exécute.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER