Partager via


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

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 :

  1. Créez un répertoire pour stocker l’exemple.

  2. Entrez la commande dotnet new console dans l’invite de commandes pour créer un projet .NET Core.

  3. Copiez et collez le code de l’exemple dans votre éditeur de code.

  4. 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, comme dotnet new, dotnet build, dotnet run, dotnet test, dotnet publish et dotnet 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.

  5. 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 de A, et C dérive de A. Le champ A._value privé est visible dans A.B. Toutefois, si vous supprimez les commentaires de la méthode C.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ée Method1, et la classe B hérite de la classe A. L’exemple appelle ensuite Method1 comme s’il s’agissait d’une méthode d’instance sur B.

    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 objet SimpleClass en sa représentation de chaîne, retourne le nom de type complet. Dans ce cas, la méthode ToString 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 static Equals(Object, Object) et la méthode public static ReferenceEquals(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 type SimpleClass.

  • 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 membres Publication fournissent des implémentations de méthode, ou si Publication 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 types Publication 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 de Periodical, qui est elle-même une classe de base de Magazine, Journal et Newspaper.

    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 de Publication, comme Magazine et Article.

  • 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 classe abstract sans méthode abstract indique que cette classe représente un concept abstrait qui est partagé entre plusieurs classes concrètes (comme Book, 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éthode abstract, mais la classe elle-même est abstract.

  • 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 est abstract, 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 constructeur Publication.

    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 et Type. Les valeurs sont à l’origine fournies par l’appel au constructeur de la classe Publication.

  • Membres liés à la publication

    Deux méthodes, Publish et GetPublicationDate définissent et retournent la date de publication. La méthode Publish définit l’indicateur privé published sur true lorsqu’elle est appelée et affecte la date passée comme argument au champ privé datePublished. La méthode GetPublicationDate retourne la chaîne « NYP » si l’indicateur published est false, et la valeur du champ datePublished si l’indicateur est true.

  • 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és CopyrightName et CopyrightDate.

  • 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.

Les classes Object et Publication

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 constructeur Publication. 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’objet Book, un numéro unique à 10 ou 13 chiffres. Le numéro ISBN est fourni en tant qu’argument à un des constructeurs Book. 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 constructeurs Book et est stocké dans la propriété.

  • Deux propriétés en lecture seule relatives au prix, Price et Currency. Leurs valeurs sont fournies comme arguments dans un appel de méthode SetPrice. 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 classe Book.

  • Une méthode SetPrice qui définit les valeurs des propriétés Price et Currency. Ces valeurs sont retournées par ces mêmes propriétés.

  • Se substitue à la méthode ToString (héritée de Publication) 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 objets Book 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 objets Book 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.

Les classes Publication et Book

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