Héritage dans C# et .NET
Ce didacticiel vous présente l’héritage dans C#. L’héritage est une fonctionnalité des langages de programmation orientés objet qui vous permet de définir une classe de base qui fournit des fonctionnalités spécifiques (données et comportement) et de définir des classes dérivées qui héritent ou substituent cette fonctionnalité.
Prérequis
- Nous vous recommandons Visual Studio pour Windows. Vous pouvez télécharger une version gratuite à partir de la page téléchargements de Visual Studio. Visual Studio inclut le Kit de développement logiciel (SDK) .NET.
- Vous pouvez également utiliser l’éditeur Visual Studio Code avec le DevKit C#. Vous devez installer le dernier SDK .NET séparément.
- Si vous préférez un autre éditeur, vous devez installer le dernier SDK .NET.
Exécution des exemples
Pour créer et exécuter les exemples de ce didacticiel, vous utilisez l’utilitaire dotnet en ligne de commande. Pour chaque exemple, procédez comme suit :
Créez un répertoire pour stocker l’exemple.
Entrez la commande dotnet new console dans l’invite de commandes pour créer un projet .NET Core.
Copiez et collez le code de l’exemple dans votre éditeur de code.
Entrez la commande dotnet restore à partir de la ligne de commande pour charger ou restaurer les dépendances du projet.
Vous n’avez pas besoin d’exécuter
dotnet restore
, car il est exécuté implicitement par toutes les commandes qui nécessitent une restauration pour se produire, commedotnet new
,dotnet build
,dotnet run
,dotnet test
,dotnet publish
etdotnet pack
. Pour désactiver la restauration implicite, utilisez l’option--no-restore
.La commande
dotnet restore
est toujours utile dans certains scénarios où la restauration explicite est logique, comme les builds d’intégration continue dans Azure DevOps Services ou dans les systèmes de génération qui doivent contrôler explicitement le moment où la restauration se produit.Pour plus d’informations sur la gestion des flux NuGet, consultez la documentation
dotnet restore
.Entrez la commande dotnet run pour compiler et exécuter l’exemple.
Présentation : Qu’est-ce que l’héritage ?
L’héritage est un des attributs fondamentaux de la programmation orientée objet. Il vous permet de définir une classe enfant qui réutilise (hérite), étend ou modifie le comportement d’une classe parente. La classe dont les membres sont hérités s’appelle la classe de base. La classe qui hérite des membres de la classe de base est appelée la classe dérivée.
C# et .NET prennent uniquement en charge l’héritage simple. C’est-à-dire qu’une classe ne peut hériter que d'une seule classe. Toutefois, l’héritage est transitif, ce qui permet de définir une hiérarchie d’héritage pour un ensemble de types. En d’autres termes, le type D
peut hériter du type C
, qui hérite du type B
, qui hérite du type de classe de base A
. Étant donné que l’héritage est transitif, les membres de type A
sont disponibles pour le type D
.
Tous les membres d’une classe de base ne sont pas hérités par les classes dérivées. Les membres suivants ne sont pas hérités :
Les constructeurs statiques, qui initialisent les données statiques d’une classe.
Les Constructeurs d’instance, que vous appelez pour créer une nouvelle instance de la classe. Chaque classe doit définir ses propres constructeurs.
Les finaliseurs, qui sont appelés par le récupérateur de mémoire du runtime pour détruire les instances d’une classe.
Bien que tous les autres membres de classe de base sont hérités par les classes dérivées, leur visibilité dépend de leur accessibilité. L’accessibilité d’un membre affecte sa visibilité pour les classes dérivées de la manière suivante :
Les membres Privés sont visibles uniquement dans les classes dérivées qui sont imbriquées dans leur classe de base. Sinon, ils ne sont pas visibles dans les classes dérivées. Dans l’exemple suivant,
A.B
est une classe imbriquée qui dérive deA
, etC
dérive deA
. Le champA._value
privé est visible dans A.B. Toutefois, si vous supprimez les commentaires de la méthodeC.GetValue
et essayez de compiler l’exemple, il génère l’erreur de compilateur CS0122 : « 'A.value est inaccessible en raison de son niveau de protection ».public class A { private int _value = 10; public class B : A { public int GetValue() { return _value; } } } public class C : A { // public int GetValue() // { // return _value; // } } public class AccessExample { public static void Main(string[] args) { var b = new A.B(); Console.WriteLine(b.GetValue()); } } // The example displays the following output: // 10
Les membres protégés sont visibles uniquement dans les classes dérivées.
Les membres internes sont visibles uniquement dans les classes dérivées qui sont trouvent dans le même assembly que la classe de base. Ils ne sont pas visibles dans les classes dérivées situées dans un autre assembly à partir de la classe de base.
Les membres publics sont visibles dans les classes dérivées et font partie de l’interface publique de la classe dérivée. Les membres publics hérités peuvent être appelés comme s’ils étaient définis dans la classe dérivée. Dans l’exemple suivant, la classe
A
définit une méthode nomméeMethod1
, et la classeB
hérite de la classeA
. L’exemple appelle ensuiteMethod1
comme s’il s’agissait d’une méthode d’instance surB
.public class A { public void Method1() { // Method implementation. } } public class B : A { } public class Example { public static void Main() { B b = new (); b.Method1(); } }
Les classes dérivées peuvent également substituer les membres hérités en fournissant une implémentation alternative. Pour être en mesure de substituer un membre, le membre de la classe de base doit être marqué avec le mot-clé virtual. Par défaut, les membres de classe de base ne sont pas marqués comme virtual
et ne peut pas être substitués. Une tentative de substituer un membre non virtuel, comme dans l’exemple suivant, génère l’erreur de compilateur CS0506 : « <member> : impossible de substituer le membre hérité <member>, car il n’est pas marqué comme virtual, abstract ou override. »
public class A
{
public void Method1()
{
// Do something.
}
}
public class B : A
{
public override void Method1() // Generates CS0506.
{
// Do something else.
}
}
Dans certains cas, une classe dérivée doit remplacer l’implémentation de la classe de base. Les membres de classe de base marqués avec le mot-clé abstract requièrent que les classes dérivées les remplacent. Compiler l’exemple suivant génère l’erreur de compilateur CS0534, « <class> does not implement inherited abstract member <member> », car la classe B
ne fournit aucune implémentation de A.Method1
.
public abstract class A
{
public abstract void Method1();
}
public class B : A // Generates CS0534.
{
public void Method3()
{
// Do something.
}
}
L’héritage s’applique uniquement aux classes et interfaces. Les autres catégories de type (structures, délégués et énumérations) ne permettent pas l’héritage. En raison de ces règles, la tentative de compilation du code comme dans l’exemple suivant génère l’erreur de compilateur CS0527 : « Le type « ValueType » dans la liste d’interface n’est pas une interface ». Le message d’erreur indique que, bien que vous puissiez définir les interfaces qu’un struct implémente, l’héritage n’est pas pris en charge.
public struct ValueStructure : ValueType // Generates CS0527.
{
}
Héritage implicite
Outre les types qui peuvent hériter via l’héritage simple, tous les types dans le système de types de .NET héritent implicitement de Object ou d’un type dérivé. Les fonctionnalités communes de Object sont disponibles pour n’importe quel type.
Pour comprendre ce que l’héritage implicite signifie, nous allons définir une nouvelle classe, SimpleClass
, qui est simplement une définition de classe vide :
public class SimpleClass
{ }
Vous pouvez ensuite utiliser la réflexion (qui permet d’inspecter les métadonnées d’un type pour obtenir des informations sur ce type) pour obtenir la liste des membres qui appartiennent au type SimpleClass
. Même si vous n’avez pas défini de membres dans votre classe SimpleClass
, la sortie de l’exemple indique qu’il a en fait neuf membres. Un de ces membres est un constructeur sans paramètre (ou par défaut) qui est fourni automatiquement pour le type SimpleClass
par le compilateur C#. Les huit restants sont membres de Object, le type à partir duquel toutes les classes et interfaces du système de type .NET héritent implicitement.
using System.Reflection;
public class SimpleClassExample
{
public static void Main()
{
Type t = typeof(SimpleClass);
BindingFlags flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public |
BindingFlags.NonPublic | BindingFlags.FlattenHierarchy;
MemberInfo[] members = t.GetMembers(flags);
Console.WriteLine($"Type {t.Name} has {members.Length} members: ");
foreach (MemberInfo member in members)
{
string access = "";
string stat = "";
var method = member as MethodBase;
if (method != null)
{
if (method.IsPublic)
access = " Public";
else if (method.IsPrivate)
access = " Private";
else if (method.IsFamily)
access = " Protected";
else if (method.IsAssembly)
access = " Internal";
else if (method.IsFamilyOrAssembly)
access = " Protected Internal ";
if (method.IsStatic)
stat = " Static";
}
string output = $"{member.Name} ({member.MemberType}): {access}{stat}, Declared by {member.DeclaringType}";
Console.WriteLine(output);
}
}
}
// The example displays the following output:
// Type SimpleClass has 9 members:
// ToString (Method): Public, Declared by System.Object
// Equals (Method): Public, Declared by System.Object
// Equals (Method): Public Static, Declared by System.Object
// ReferenceEquals (Method): Public Static, Declared by System.Object
// GetHashCode (Method): Public, Declared by System.Object
// GetType (Method): Public, Declared by System.Object
// Finalize (Method): Internal, Declared by System.Object
// MemberwiseClone (Method): Internal, Declared by System.Object
// .ctor (Constructor): Public, Declared by SimpleClass
L’héritage implicite à partir de la classe Object rend ces méthodes disponibles pour la classe SimpleClass
:
La méthode public
ToString
, qui convertit un objetSimpleClass
en sa représentation de chaîne, retourne le nom de type complet. Dans ce cas, la méthodeToString
retourne la chaîne « SimpleClass ».Voici trois méthodes de test d’égalité de deux objets : la méthode public instance
Equals(Object)
, la méthode public staticEquals(Object, Object)
et la méthode public staticReferenceEquals(Object, Object)
. Par défaut, ces méthodes testent l’égalité des références. Autrement dit, pour être égales, deux variables d’objet doivent faire référence au même objet.La méthode public
GetHashCode
, qui calcule une valeur qui permet à une instance du type d’être utilisée dans des collections hachées.La méthode
GetType
publique qui retourne un objet Type qui représente le typeSimpleClass
.La méthode protected Finalize, qui est conçue pour libérer les ressources non gérées avant que la mémoire d’un objet soit récupérée par le récupérateur de mémoire.
La méthode protected MemberwiseClone, qui crée un clone partiel de l’objet actuel.
En raison de l’héritage implicite, vous pouvez appeler n’importe quel membre hérité d’un objet SimpleClass
exactement comme s’il était en fait un membre défini dans la classe SimpleClass
. Par exemple, l’exemple suivant appelle la méthode SimpleClass.ToString
, dont SimpleClass
hérite de Object.
public class EmptyClass
{ }
public class ClassNameExample
{
public static void Main()
{
EmptyClass sc = new();
Console.WriteLine(sc.ToString());
}
}
// The example displays the following output:
// EmptyClass
Le tableau suivant répertorie les catégories de types que vous pouvez créer en C# et les types à partir desquels ils héritent implicitement. Chaque type de base apporte un autre ensemble de membres disponibles via l’héritage aux types dérivés implicitement.
Catégorie de type | Hérite implicitement de |
---|---|
class | Object |
struct | ValueType, Object |
enum | Enum, ValueType, Object |
délégué | MulticastDelegate, Delegate, Object |
L’héritage et une relation « est un »
En règle générale, l’héritage est utilisé pour exprimer une relation « est un » entre une classe de base et une ou plusieurs classes dérivées, où les classes dérivées sont des versions spécialisées de la classe de base ; la classe dérivée est un type de la classe de base. Par exemple, la classe Publication
représente une publication de tout type et les classes Book
et Magazine
représentent les classes des types spécifiques de publications.
Notes
Les classes et structures peuvent implémenter une ou plusieurs interfaces. Bien que l’implémentation d’interface est souvent présentée comme une solution de contournement pour l’héritage unique ou comme une façon d’utiliser l’héritage avec les structures, elle est conçue pour exprimer une autre relation (« peut faire ») entre une interface et son type d’implémentation que l’héritage. Une interface définit un sous-ensemble de fonctionnalités (comme la capacité à tester l’égalité, comparer ou trier des objets, ou pour prendre en charge la mise en forme et l’analyse dépendant de la culture) que l’interface met à disposition pour ses types d’implémentation.
Notez que « est un » exprime également la relation entre un type et une instanciation spécifique de ce type. Dans l’exemple suivant, Automobile
est une classe qui possède trois propriétés en lecture seule uniques : Make
, le fabricant de l’automobile ; Model
, le type de voiture et Year
, son année de fabrication. Votre classe Automobile
comporte également un constructeur dont les arguments sont assignés aux valeurs de propriété, et elle remplace la méthode Object.ToString pour générer une chaîne qui identifie de façon unique l’instance Automobile
plutôt que la classe Automobile
.
public class Automobile
{
public Automobile(string make, string model, int year)
{
if (make == null)
throw new ArgumentNullException(nameof(make), "The make cannot be null.");
else if (string.IsNullOrWhiteSpace(make))
throw new ArgumentException("make cannot be an empty string or have space characters only.");
Make = make;
if (model == null)
throw new ArgumentNullException(nameof(model), "The model cannot be null.");
else if (string.IsNullOrWhiteSpace(model))
throw new ArgumentException("model cannot be an empty string or have space characters only.");
Model = model;
if (year < 1857 || year > DateTime.Now.Year + 2)
throw new ArgumentException("The year is out of range.");
Year = year;
}
public string Make { get; }
public string Model { get; }
public int Year { get; }
public override string ToString() => $"{Year} {Make} {Model}";
}
Dans ce cas, vous ne devriez pas vous reposer sur l’héritage pour représenter les modèles et constructeurs spécifiques. Par exemple, il est inutile de définir un type Packard
pour représenter les véhicules automobiles fabriqués par la société Packard Motor Car. Au lieu de cela, vous pouvez les représenter en créant un objet Automobile
avec les valeurs appropriées passées à son constructeur de classe, comme dans l’exemple suivant.
using System;
public class Example
{
public static void Main()
{
var packard = new Automobile("Packard", "Custom Eight", 1948);
Console.WriteLine(packard);
}
}
// The example displays the following output:
// 1948 Packard Custom Eight
Une relation « est un » basée sur l’héritage est préférablement appliquée à une classe de base et aux classes dérivées qui ajoutent des membres supplémentaires à la classe de base ou qui nécessitent des fonctionnalités supplémentaires non présentes dans la classe de base.
Conception de la classe de base et des classes dérivées
Examinons le processus de conception d’une classe de base et de ses classes dérivées. Dans cette section, vous allez définir une classe de base, Publication
, qui représente une publication de tout type, comme un livre, un magazine, un journal, une revue, un article, etc. Vous allez également définir une classe Book
dérivée de Publication
. Vous pourriez facilement étendre l’exemple pour définir d’autres classes dérivées, comme Magazine
, Journal
, Newspaper
et Article
.
Classe Publication de base
Lors de la conception de votre classe Publication
, vous devez prendre plusieurs décisions de conception :
Les membres à inclure dans votre classe de base
Publication
et si les membresPublication
fournissent des implémentations de méthode, ou siPublication
est une classe de base abstraite qui sert de modèle pour ses classes dérivées.Dans ce cas, la classe
Publication
fournit des implémentations de méthode. La section Conception de classes de base abstraites et leurs classes dérivées contient un exemple qui utilise une classe de base abstraite pour définir les méthodes que les classes dérivées doivent substituer. Les classes dérivées sont libres de fournir une implémentation qui convient pour le type dérivé.La possibilité de réutiliser le code (autrement dit, plusieurs classes dérivées partagent la déclaration et l’implémentation de méthodes de classe de base et n’ont pas besoin de les substituer) constitue un avantage des classes de base non abstraites. Par conséquent, vous devez ajouter des membres à
Publication
si leur code est susceptible d’être partagé par certains ou la majorité des typesPublication
spécialisés. Si vous ne fournissez pas efficacement les implémentations de classe de base, vous devrez fournir des implémentations de membres en grande partie identiques dans les classes dérivées au lieu d’une implémentation unique dans la classe de base. La nécessité de maintenir le code dupliqué à plusieurs emplacements est une source potentielle de bogues.Pour optimiser la réutilisation du code et créer une hiérarchie d’héritage logique et intuitive, vous voulez vous assurer d’ajouter à la classe
Publication
uniquement les données et fonctionnalités communes à toutes les publications ou la plupart d’entre elles. Les classes dérivées implémentent ensuite les membres qui sont uniques pour les types particuliers de publications qu’ils représentent.La mesure dans laquelle étendre votre hiérarchie de classes. Souhaitez-vous développer une hiérarchie de trois classes ou plus, plutôt que simplement une classe de base et une ou plusieurs classes dérivées ? Par exemple,
Publication
peut être une classe de base dePeriodical
, qui est elle-même une classe de base deMagazine
,Journal
etNewspaper
.Dans votre exemple, vous allez utiliser la hiérarchie simple d’une classe
Publication
et d’une classe dérivée unique,Book
. Vous pourriez facilement étendre l’exemple pour créer un certain nombre d’autres classes qui dérivent dePublication
, commeMagazine
etArticle
.S’il est judicieux d’instancier la classe de base. Si ce n’est pas le cas, vous devez appliquer le mot-clé abstract à la classe. Dans le cas contraire, votre classe
Publication
peut être instanciée en appelant son constructeur de classe. Si une tentative est effectuée pour instancier une classe marquée avec le mot cléabstract
par un appel direct à son constructeur de classe, le compilateur C# génère l’erreur CS0144 « Impossible de créer une instance de l’interface ou de la classe abstraite ». Si une tentative d’instanciation de la classe à l’aide de la réflexion est effectuée, la méthode de réflexion lève une exception MemberAccessException.Par défaut, une classe de base peut être instanciée en appelant son constructeur de classe. Vous n’avez pas à définir explicitement un constructeur de classe. S’il n’y en a aucun présent dans le code source de la classe de base, le compilateur C# fournit automatiquement un constructeur par défaut (sans paramètre).
Dans votre exemple, vous marquez la classe
Publication
comme abstract afin qu’elle ne puisse pas être instanciée. Une classeabstract
sans méthodeabstract
indique que cette classe représente un concept abstrait qui est partagé entre plusieurs classes concrètes (commeBook
,Journal
).Si les classes dérivées doivent hériter de l’implémentation de classe de base de membres particuliers, si elles ont l’option de substituer l’implémentation de la classe de base ou si elles doivent fournir une implémentation. Vous utilisez le mot clé abstract pour forcer les classes dérivées à fournir une implémentation. Vous devez utiliser le mot clé virtual pour permettre aux classes dérivées de substituer une méthode de classe de base. Par défaut, les méthodes définies dans la classe de base ne sont pas substituables.
La classe
Publication
n’a aucune méthodeabstract
, mais la classe elle-même estabstract
.Si une classe dérivée représente la classe finale dans la hiérarchie d’héritage et ne peut pas elle-même être utilisée comme classe de base pour les classes dérivées supplémentaires. Par défaut, toute classe peut servir de classe de base. Vous pouvez appliquer le mot clé sealed pour indiquer qu’une classe ne peut pas servir de classe de base pour des classes supplémentaires. La tentative de dériver à partir d’une classe sealed a généré l’erreur de compilateur CS0509, « impossible de dériver à partir du type sealed <typeName> ».
Dans votre exemple, vous allez marquer votre classe dérivée en tant que
sealed
.
L’exemple suivant montre le code source pour la classe Publication
ainsi qu’une énumération PublicationType
retournée par la propriété Publication.PublicationType
. Outre les membres qu’elle hérite de Object, la classe Publication
définit les membres uniques et substitutions de membres suivants :
public enum PublicationType { Misc, Book, Magazine, Article };
public abstract class Publication
{
private bool _published = false;
private DateTime _datePublished;
private int _totalPages;
public Publication(string title, string publisher, PublicationType type)
{
if (string.IsNullOrWhiteSpace(publisher))
throw new ArgumentException("The publisher is required.");
Publisher = publisher;
if (string.IsNullOrWhiteSpace(title))
throw new ArgumentException("The title is required.");
Title = title;
Type = type;
}
public string Publisher { get; }
public string Title { get; }
public PublicationType Type { get; }
public string? CopyrightName { get; private set; }
public int CopyrightDate { get; private set; }
public int Pages
{
get { return _totalPages; }
set
{
if (value <= 0)
throw new ArgumentOutOfRangeException(nameof(value), "The number of pages cannot be zero or negative.");
_totalPages = value;
}
}
public string GetPublicationDate()
{
if (!_published)
return "NYP";
else
return _datePublished.ToString("d");
}
public void Publish(DateTime datePublished)
{
_published = true;
_datePublished = datePublished;
}
public void Copyright(string copyrightName, int copyrightDate)
{
if (string.IsNullOrWhiteSpace(copyrightName))
throw new ArgumentException("The name of the copyright holder is required.");
CopyrightName = copyrightName;
int currentYear = DateTime.Now.Year;
if (copyrightDate < currentYear - 10 || copyrightDate > currentYear + 2)
throw new ArgumentOutOfRangeException($"The copyright year must be between {currentYear - 10} and {currentYear + 1}");
CopyrightDate = copyrightDate;
}
public override string ToString() => Title;
}
Un constructeur
Étant donné que la classe
Publication
estabstract
, elle ne peut pas être instanciée directement à partir du code comme dans l’exemple suivant :var publication = new Publication("Tiddlywinks for Experts", "Fun and Games", PublicationType.Book);
Toutefois, son constructeur d’instance peut être appelé directement à partir des constructeurs de classes dérivées, comme le code source pour la classe
Book
le montre.Deux propriétés liées à la publication
Title
est une propriété String en lecture seule dont la valeur est fournie en appelant le constructeurPublication
.Pages
est une propriété Int32 en lecture seule qui indique le nombre total de pages de la publication. La valeur est stockée dans un champ privé nommétotalPages
. Elle doit être positive, sans quoi une exception ArgumentOutOfRangeException est levée.Membres liés à l’éditeur
Deux propriétés en lecture seule,
Publisher
etType
. Les valeurs sont à l’origine fournies par l’appel au constructeur de la classePublication
.Membres liés à la publication
Deux méthodes,
Publish
etGetPublicationDate
définissent et retournent la date de publication. La méthodePublish
définit l’indicateur privépublished
surtrue
lorsqu’elle est appelée et affecte la date passée comme argument au champ privédatePublished
. La méthodeGetPublicationDate
retourne la chaîne « NYP » si l’indicateurpublished
estfalse
, et la valeur du champdatePublished
si l’indicateur esttrue
.Membres liés aux droits d’auteur
La méthode
Copyright
prend comme arguments le nom du titulaire des droits d’auteur et l’année des droits d’auteur et les attribue aux propriétésCopyrightName
etCopyrightDate
.Une substitution de la méthode
ToString
Si un type ne remplace pas la méthode Object.ToString, il retourne le nom qualifié complet du type, ce qui n’aide pas vraiment à faire la différence entre une instance et une autre. La classe
Publication
substitue Object.ToString pour retourner la valeur de la propriétéTitle
.
Le schéma suivant illustre la relation entre votre classe Publication
de base et sa classe Object implicitement héritée.
La classe Book
La classe Book
représente un livre sous la forme d’un type spécialisé de publication. L’exemple suivant montre le code source pour la classe Book
.
using System;
public sealed class Book : Publication
{
public Book(string title, string author, string publisher) :
this(title, string.Empty, author, publisher)
{ }
public Book(string title, string isbn, string author, string publisher) : base(title, publisher, PublicationType.Book)
{
// isbn argument must be a 10- or 13-character numeric string without "-" characters.
// We could also determine whether the ISBN is valid by comparing its checksum digit
// with a computed checksum.
//
if (!string.IsNullOrEmpty(isbn))
{
// Determine if ISBN length is correct.
if (!(isbn.Length == 10 | isbn.Length == 13))
throw new ArgumentException("The ISBN must be a 10- or 13-character numeric string.");
if (!ulong.TryParse(isbn, out _))
throw new ArgumentException("The ISBN can consist of numeric characters only.");
}
ISBN = isbn;
Author = author;
}
public string ISBN { get; }
public string Author { get; }
public decimal Price { get; private set; }
// A three-digit ISO currency symbol.
public string? Currency { get; private set; }
// Returns the old price, and sets a new price.
public decimal SetPrice(decimal price, string currency)
{
if (price < 0)
throw new ArgumentOutOfRangeException(nameof(price), "The price cannot be negative.");
decimal oldValue = Price;
Price = price;
if (currency.Length != 3)
throw new ArgumentException("The ISO currency symbol is a 3-character string.");
Currency = currency;
return oldValue;
}
public override bool Equals(object? obj)
{
if (obj is not Book book)
return false;
else
return ISBN == book.ISBN;
}
public override int GetHashCode() => ISBN.GetHashCode();
public override string ToString() => $"{(string.IsNullOrEmpty(Author) ? "" : Author + ", ")}{Title}";
}
Outre les membres qu’elle hérite de Publication
, la classe Book
définit les membres uniques et substitutions de membres suivants :
Deux constructeurs
Les deux constructeurs
Book
partagent trois paramètres communs. Deux d’entre eux, title et publisher, correspondent aux paramètres du constructeurPublication
. Le troisième est author, qui est stocké dans une propriétéAuthor
non modifiable publique. Un constructeur inclut un paramètre isbn, qui est stocké dans l’auto-propriétéISBN
.Le premier constructeur utilise le mot-clé this pour appeler l’autre constructeur. Le chaînage de constructeurs est un modèle courant pour la définition de constructeurs. Les constructeurs avec le moins de paramètres fournissent les valeurs par défaut au moment de l’appel du constructeur avec le plus grand nombre de paramètres.
Le deuxième constructeur utilise le mot-clé base pour transmettre le titre et le nom de l’éditeur au constructeur de classe de base. Si vous n’effectuez pas un appel explicite à un constructeur de classe de base dans votre code source, le compilateur C# fournit automatiquement un appel au constructeur par défaut ou sans paramètres de la classe de base.
Une propriété
ISBN
en lecture seule qui retourne le numéro ISBN de l’objetBook
, un numéro unique à 10 ou 13 chiffres. Le numéro ISBN est fourni en tant qu’argument à un des constructeursBook
. Le numéro ISBN est stocké dans un champ de stockage privé qui est généré automatiquement par le compilateur.Une propriété
Author
en lecture seule. Le nom de l’auteur est fourni en tant qu’argument aux deux constructeursBook
et est stocké dans la propriété.Deux propriétés en lecture seule relatives au prix,
Price
etCurrency
. Leurs valeurs sont fournies comme arguments dans un appel de méthodeSetPrice
. La propriétéCurrency
est le symbole de devise ISO à trois caractères (par exemple, USD pour le dollar américain). Les symboles de devise ISO peuvent être récupérés à partir de la propriété ISOCurrencySymbol. Ces deux propriétés sont en lecture seule en externe, mais peuvent être définies par du code dans la classeBook
.Une méthode
SetPrice
qui définit les valeurs des propriétésPrice
etCurrency
. Ces valeurs sont retournées par ces mêmes propriétés.Se substitue à la méthode
ToString
(héritée dePublication
) et aux méthodes Object.Equals(Object) et GetHashCode (héritées de Object).Sauf si elle est substituée, la méthode Object.Equals(Object) teste l’égalité des références. Autrement dit, deux variables d’objet sont considérées comme égales si elles font référence au même objet. Dans la classe
Book
, en revanche, deux objetsBook
doivent être égaux s’ils ont le même ISBN.Lorsque vous substituez la méthode Object.Equals(Object), vous devez également substituer la méthode GetHashCode qui retourne une valeur que le runtime utilise pour stocker les éléments dans les collections hachées pour une récupération efficace. Le code de hachage doit retourner une valeur qui est cohérente avec le test d’égalité. Étant donné que vous avez substitué Object.Equals(Object) pour retourner
true
si les propriétés ISBN de deux objetsBook
sont égales, vous retournez le code de hachage calculé en appelant la méthode GetHashCode de la chaîne retournée par la propriétéISBN
.
Le schéma suivant illustre la relation entre la base la classe Book
et Publication
, sa classe de base.
Vous pouvez maintenant instancier un objet Book
, appeler ses membres uniques et hérités le passer en tant qu’argument pour une méthode qui attend un paramètre de type Publication
ou de type Book
, comme illustré dans l’exemple suivant.
public class ClassExample
{
public static void Main()
{
var book = new Book("The Tempest", "0971655819", "Shakespeare, William",
"Public Domain Press");
ShowPublicationInfo(book);
book.Publish(new DateTime(2016, 8, 18));
ShowPublicationInfo(book);
var book2 = new Book("The Tempest", "Classic Works Press", "Shakespeare, William");
Console.Write($"{book.Title} and {book2.Title} are the same publication: " +
$"{((Publication)book).Equals(book2)}");
}
public static void ShowPublicationInfo(Publication pub)
{
string pubDate = pub.GetPublicationDate();
Console.WriteLine($"{pub.Title}, " +
$"{(pubDate == "NYP" ? "Not Yet Published" : "published on " + pubDate):d} by {pub.Publisher}");
}
}
// The example displays the following output:
// The Tempest, Not Yet Published by Public Domain Press
// The Tempest, published on 8/18/2016 by Public Domain Press
// The Tempest and The Tempest are the same publication: False
Conception de classes de base abstraites et de leurs classes dérivées
Dans l’exemple précédent, vous avez défini une classe de base qui a fourni une implémentation d’un certain nombre de méthodes pour permettre aux classes dérivées de partager du code. Dans de nombreux cas, toutefois, la classe de base n'est pas censée fournir une implémentation. Au lieu de cela, la classe de base est une classe abstraite qui déclare des méthodes abstraites ; elle sert de modèle qui définit les membres que chaque classe dérivée doit implémenter. En général, dans une classe de base abstraite, l’implémentation de chaque type dérivé est unique pour ce type. Vous avez marqué la classe avec le mot clé abstract, car il n’était pas judicieux d’instancier un objet Publication
, même si la classe fournissait des implémentations de fonctionnalités communes aux publications.
Par exemple, chaque forme géométrique bidimensionnelle fermée inclut deux propriétés : l’aire, l’étendue interne de la forme, et le périmètre, ou la distance le long des bords de la forme. La façon de laquelle ces propriétés sont calculées, cependant, dépend entièrement de la forme spécifique. La formule pour calculer le périmètre (ou la circonférence) d’un cercle, par exemple, est différente de celle d’un carré. La classe Shape
est une classe abstract
avec des méthodes abstract
. Ceci indique que les classes dérivées partagent les mêmes fonctionnalités, mais ces classes dérivées implémentent ces fonctionnalités différemment.
L’exemple suivant définit une classe de base abstraite nommée Shape
qui définit deux propriétés : Area
et Perimeter
. En plus de marquer la classe avec le mot clé abstract, chaque membre de l’instance est également marqué avec le mot clé abstract. Dans ce cas, Shape
substitue également la méthode Object.ToString pour renvoyer le nom du type, plutôt que son nom qualifié complet. Elle définit aussi deux membres statiques, GetArea
et GetPerimeter
, qui permettent aux appelants de récupérer facilement l’aire et le périmètre d’une instance de toute classe dérivée. Lorsque vous passez une instance d’une classe dérivée à l’une de ces méthodes, le runtime appelle la substitution de la méthode de la classe dérivée.
public abstract class Shape
{
public abstract double Area { get; }
public abstract double Perimeter { get; }
public override string ToString() => GetType().Name;
public static double GetArea(Shape shape) => shape.Area;
public static double GetPerimeter(Shape shape) => shape.Perimeter;
}
Vous pouvez ensuite dériver des classes qui représentent des formes spécifiques à partir de Shape
. L’exemple suivant définit trois classes : Square
, Rectangle
et Circle
. Chaque forme utilise une formule unique pour calculer l’aire et périmètre. Certaines des classes dérivées définissent également des propriétés, telles que Rectangle.Diagonal
et Circle.Diameter
, qui sont propres à la forme qu’ils représentent.
using System;
public class Square : Shape
{
public Square(double length)
{
Side = length;
}
public double Side { get; }
public override double Area => Math.Pow(Side, 2);
public override double Perimeter => Side * 4;
public double Diagonal => Math.Round(Math.Sqrt(2) * Side, 2);
}
public class Rectangle : Shape
{
public Rectangle(double length, double width)
{
Length = length;
Width = width;
}
public double Length { get; }
public double Width { get; }
public override double Area => Length * Width;
public override double Perimeter => 2 * Length + 2 * Width;
public bool IsSquare() => Length == Width;
public double Diagonal => Math.Round(Math.Sqrt(Math.Pow(Length, 2) + Math.Pow(Width, 2)), 2);
}
public class Circle : Shape
{
public Circle(double radius)
{
Radius = radius;
}
public override double Area => Math.Round(Math.PI * Math.Pow(Radius, 2), 2);
public override double Perimeter => Math.Round(Math.PI * 2 * Radius, 2);
// Define a circumference, since it's the more familiar term.
public double Circumference => Perimeter;
public double Radius { get; }
public double Diameter => Radius * 2;
}
L'exemple suivant utilise les objets dérivés de Shape
. Elle instancie un tableau d’objets dérivés de Shape
et appelle les méthodes statiques de la classe Shape
qui encapsule les valeurs de propriété de retour de Shape
. Le runtime récupère les valeurs de propriétés substituées des types dérivés. L’exemple convertit également chaque objet Shape
dans le tableau en son type dérivé et, si la conversion réussit, récupère les propriétés de cette sous-classe particulière de Shape
.
using System;
public class Example
{
public static void Main()
{
Shape[] shapes = { new Rectangle(10, 12), new Square(5),
new Circle(3) };
foreach (Shape shape in shapes)
{
Console.WriteLine($"{shape}: area, {Shape.GetArea(shape)}; " +
$"perimeter, {Shape.GetPerimeter(shape)}");
if (shape is Rectangle rect)
{
Console.WriteLine($" Is Square: {rect.IsSquare()}, Diagonal: {rect.Diagonal}");
continue;
}
if (shape is Square sq)
{
Console.WriteLine($" Diagonal: {sq.Diagonal}");
continue;
}
}
}
}
// The example displays the following output:
// Rectangle: area, 120; perimeter, 44
// Is Square: False, Diagonal: 15.62
// Square: area, 25; perimeter, 20
// Diagonal: 7.07
// Circle: area, 28.27; perimeter, 18.85