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 remplacent cette fonctionnalité.

Conditions préalables

Instructions d’installation

Sous Windows, ce fichier de configuration WinGet permet d'installer tous les prérequis. Si vous avez déjà installé quelque chose, WinGet ignore cette étape.

  1. Téléchargez le fichier et double-cliquez pour l’exécuter.
  2. Lisez le contrat de licence, tapez et, puis sélectionnez Entrer lorsque vous êtes invité à accepter.
  3. Si vous recevez une invite de contrôle de compte d’utilisateur (UAC) qui clignote dans votre barre des tâches, autorisez la poursuite de l'installation.

Sur d’autres plateformes, vous devez installer chacun de ces composants séparément.

  1. Téléchargez le programme d’installation recommandé à partir de la page de téléchargement du Kit de développement logiciel (SDK) .NET et double-cliquez pour l’exécuter. La page de téléchargement détecte votre plateforme et recommande le dernier programme d’installation de votre plateforme.
  2. Téléchargez le dernier programme d’installation à partir de la page d’accueil Visual Studio Code, puis double-cliquez pour l’exécuter. Cette page détecte également votre plateforme et le lien doit être correct pour votre système.
  3. Cliquez sur le bouton « Installer » dans la page d’extension devKit C#. Cela ouvre Visual Studio Code et vous demande si vous souhaitez installer ou activer l’extension. Sélectionnez « installer ».

Exécution des exemples

Pour créer et exécuter les exemples de ce didacticiel, vous utilisez l’utilitaire dotnet à partir de la ligne de commande. Procédez comme suit pour chaque exemple :

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

  2. Entrez la commande dotnet new console dans l'invite de commande 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.

Arrière-plan : 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 classe dérivée.

C# et .NET prennent uniquement en charge l’héritage simple. Autrement dit, une classe ne peut hériter qu’d’une seule classe. Toutefois, l’héritage est transitif, ce qui vous 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 des classes dérivées. Les membres suivants ne sont pas hérités :

  • 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 d’une classe de base soient hérités par des classes dérivées, qu’ils soient visibles ou non dépendent de leur accessibilité. L’accessibilité d’un membre affecte sa visibilité pour les classes dérivées comme suit :

  • Les membres privés sont visibles uniquement dans les classes dérivées qui sont imbriquées dans leur classe de base. Sinon, elles 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 de A._value privé est visible dans A.B. Toutefois, si vous supprimez les commentaires de la méthode C.GetValue et tentez de compiler l’exemple, il génère l’erreur du 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 assembly différent 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 pouvoir remplacer un membre, le membre de la classe de base doit être marqué avec le mot clé virtuel. Par défaut, les membres de classe de base ne sont pas marqués comme virtual et ne peuvent pas être substitués. La tentative de remplacement d’un membre non virtuel, comme l’exemple suivant, génère l’erreur du compilateur CS0506 : «<membre> ne peut pas remplacer le membre hérité <membre>, car il n’est pas marqué comme virtuel, abstrait ou substitué ».

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 classe de base. Les membres de classe de base marqués avec le mot clé abstrait nécessitent que les classes dérivées les remplacent. La tentative de compilation de l’exemple suivant génère l’erreur du compilateur CS0534 , «<classe> n’implémente pas le membre abstrait hérité <membre>», car la classe B ne fournit aucune implémentation pour 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 l’exemple suivant génère l’erreur du compilateur CS0527 : « 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 tous les types qu’ils peuvent hériter par le biais d’un héritage unique, tous les types du système de type .NET héritent implicitement de Object ou d’un type dérivé de celui-ci. Les fonctionnalités courantes de Object sont disponibles pour n’importe quel type.

Pour voir ce que signifie l’héritage implicite, 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 vous permet d’inspecter les métadonnées d’un type pour obtenir des informations sur ce type) pour obtenir une liste des membres qui appartiennent au type SimpleClass. Bien que vous n’ayez défini aucun membre dans votre classe SimpleClass, la sortie de l’exemple indique qu’elle comporte en fait neuf membres. L’un de ces membres est un constructeur sans paramètre (ou par défaut) qui est automatiquement fourni 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 de la classe Object met ces méthodes à la disposition de la classe SimpleClass :

  • La méthode ToString publique, qui convertit un objet SimpleClass en sa représentation sous forme de chaîne, retourne le nom de type complet. Dans ce cas, la méthode ToString retourne la chaîne « SimpleClass ».

  • Trois méthodes qui testent l’égalité de deux objets : l’instance publique Equals(Object) méthode, la méthode Equals(Object, Object) statique publique et la méthode ReferenceEquals(Object, Object) statique publique. Par défaut, ces méthodes testent l’égalité des références ; autrement dit, pour être égale, deux variables d’objet doivent faire référence au même objet.

  • Méthode de GetHashCode publique, qui calcule une valeur qui permet à une instance du type d’être utilisée dans les collections hachées.

  • Méthode GetType publique, qui retourne un objet Type qui représente le type SimpleClass.

  • La méthode protégée Finalize, conçue pour libérer les ressources non gérées avant que la mémoire d'un objet ne soit récupérée par le ramasse-miettes.

  • Méthode MemberwiseClone protégée, qui crée un clone peu profond de l’objet actuel.

En raison de l’héritage implicite, vous pouvez appeler n’importe quel membre hérité d’un objet SimpleClass comme s’il s’agissait réellement d’un membre défini dans la classe SimpleClass. Par exemple, l’exemple suivant appelle la méthode SimpleClass.ToString, qui 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
classe Object
Struct ValueType, Object
énumération Enum, ValueType, Object
déléguer 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 « is a » 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 n’importe quel type, et les classes Book et Magazine représentent des types spécifiques de publications.

Remarque

Une classe ou un struct peut 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 (telles que la possibilité de tester l’égalité, de comparer ou de trier des objets, ou de prendre en charge l’analyse et la mise en forme sensibles à la culture) que l’interface met à la disposition de ses types d’implémentation.

Notez que « est un » exprime également la relation entre un type et une instance spécifique de ce type. Dans l’exemple suivant, Automobile est une classe qui a trois propriétés en lecture seule uniques : Make, le fabricant de l’automobile ; Model, le genre d’automobile ; et Year, son année de fabrication. Votre classe Automobile a également un constructeur dont les arguments sont affectés aux valeurs de propriété et remplace la méthode Object.ToString pour produire une chaîne qui identifie de manière 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 devez pas compter sur l’héritage pour représenter des modèles et des voitures spécifiques. Par exemple, vous n’avez pas besoin de définir un type de Packard pour représenter les automobiles fabriquées par packard Motor Car Company. 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 is-a basée sur l’héritage est mieux 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 qui ne sont pas 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, un journal, un article, etc. Vous allez également définir une classe Book qui dérive de Publication. Vous pouvez facilement étendre l’exemple pour définir d’autres classes dérivées, telles que Magazine, Journal, Newspaperet Article.

Classe Publication de base

Dans la conception de votre classe Publication, vous devez prendre plusieurs décisions de conception :

  • Quels 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 Conception de classes de base abstraites et de leurs classes dérivées section contient un exemple qui utilise une classe de base abstraite pour définir les méthodes que les classes dérivées doivent remplacer. Les classes dérivées sont libres de fournir toute implémentation adaptée au type dérivé.

    La possibilité de réutiliser du code (autrement dit, plusieurs classes dérivées partagent la déclaration et l’implémentation des méthodes de classe de base et n’ont pas besoin de les remplacer) est 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 plupart des types Publication spécialisés. Si vous ne parvenez pas à fournir efficacement des implémentations de classe de base, vous devrez fournir des implémentations membres largement identiques dans des classes dérivées plutôt qu’une seule implémentation dans la classe de base. La nécessité de maintenir du code en double dans plusieurs emplacements est une source potentielle de bogues.

    Pour optimiser la réutilisation du code et pour créer une hiérarchie d’héritage logique et intuitive, vous souhaitez être sûr que vous incluez dans la classe Publication uniquement les données et les fonctionnalités communes à toutes ou à la plupart des publications. Les classes dérivées implémentent ensuite des membres uniques aux types particuliers de publication qu’ils représentent.

  • Jusqu’à quel point étendre votre hiérarchie de classes. Voulez-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 à son tour est une classe de base de Magazine, Journal et Newspaper.

    Pour votre exemple, vous allez utiliser la petite hiérarchie d’une classe Publication et d’une classe dérivée unique, Book. Vous pouvez facilement étendre l’exemple pour créer un certain nombre de classes supplémentaires qui dérivent de Publication, telles que Magazine et Article.

  • Indique 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. Sinon, votre classe Publication peut être instanciée en appelant son constructeur de classe. Si vous tentez d'instancier une classe marquée avec le mot clé abstract par un appel direct à son constructeur, le compilateur C# génère l’erreur CS0144, « Impossible de créer une instance de la classe ou de l’interface abstraite ». Si vous tentez d'instancier la classe à l’aide de la réflexion, la méthode de réflexion lève une MemberAccessException.

    Par défaut, une classe de base peut être instanciée en appelant son constructeur de classe. Vous n’avez pas besoin de définir explicitement un constructeur de classe. Si l’un n’est pas présent dans le code source de la classe de base, le compilateur C# fournit automatiquement un constructeur par défaut (sans paramètre).

    Pour votre exemple, vous marquez la classe Publication comme abstraite afin qu’elle ne puisse pas être instanciée. Une classe abstract sans méthodes abstract indique que cette classe représente un concept abstrait partagé entre plusieurs classes concrètes (comme un Book, Journal).

  • Si les classes dérivées doivent hériter de l’implémentation de classe de base de membres particuliers, qu’elles aient la possibilité de remplacer l’implémentation de classe de base ou si elles doivent fournir une implémentation. Vous utilisez le mot clé abstrait pour forcer les classes dérivées à fournir une implémentation. Vous utilisez le mot clé virtuel pour permettre aux classes dérivées de remplacer 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 pas de méthodes abstract, mais la classe elle-même est abstract.

  • Indique si une classe dérivée représente la classe finale dans la hiérarchie d’héritage et ne peut pas être utilisée en tant que classe de base pour des classes dérivées supplémentaires. Par défaut, n’importe quelle classe peut servir de classe de base. Vous pouvez appliquer le mot clé scellé pour indiquer qu’une classe ne peut pas servir de classe de base pour toutes les classes supplémentaires. Tentative de dériver d'une classe scellée a entraîné l'erreur du compilateur CS0509 : "ne peut pas dériver du type scellé <typeName>".

    Pour votre exemple, vous marquez votre classe dérivée comme sealed.

L’exemple suivant montre le code source de 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;
}
  • 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 de constructeurs de classes dérivées, comme le code source de la classe Book 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-écriture qui indique le nombre total de pages de la composition. 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 initialement fournies par l’appel au constructeur de 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 de published est falseet la valeur du champ datePublished s’il est true.

  • Membres liés au droit d’auteur

    La méthode Copyright prend le nom du titulaire du droit d’auteur et l’année du droit d’auteur comme arguments et les assigne 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 complet du type, qui est peu utilisé pour différencier une instance d’une autre. La classe Publication remplace Object.ToString pour retourner la valeur de la propriété Title.

La figure suivante illustre la relation entre votre classe de base Publication et sa classe Object héritée implicitement.

Les classes Object et Publication

La classe Book

La classe Book représente un livre en tant que type de publication spécialisé. L’exemple suivant montre le code source de 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 moins de paramètres fournissent des valeurs par défaut lors 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 d’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ètre de la classe de base.

  • Propriété ISBN en lecture seule, qui renvoie le numéro de livre standard international de l’objet Book, un nombre unique à 10 ou 13 chiffres. Le numéro ISBN est fourni en tant qu’argument à un des constructeurs Book. L’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 en tant qu’arguments dans un appel de méthode SetPrice. La propriété Currency est le symbole monétaire ISO à trois chiffres (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 externe, mais les deux peuvent être définies par code dans la classe Book.

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

  • Remplace la méthode ToString (héritée de Publication) et les méthodes Object.Equals(Object) et GetHashCode (héritées de Object).

    À moins qu’elle ne soit remplacée, la méthode Object.Equals(Object) teste l’égalité de référence. 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, d’autre part, deux objets Book doivent être égaux s’ils ont le même ISBN.

    Lorsque vous remplacez la méthode Object.Equals(Object), vous devez également remplacer la méthode GetHashCode, qui retourne une valeur que le runtime utilise pour stocker des éléments dans des collections hachées pour une récupération efficace. Le code de hachage doit retourner une valeur cohérente avec le test d’égalité. Étant donné que vous avez remplacé 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.

La figure suivante illustre la relation entre la classe Book et Publication, sa classe de base.

cours de publication et de livre Les classes Publication et Book

Vous pouvez maintenant instancier un objet Book, appeler à la fois ses membres uniques et hérités, et le transmettre en tant qu’argument à une méthode qui attend un paramètre de type Publication ou de type Book, comme l’illustre 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 pour un certain nombre de méthodes afin d’autoriser les classes dérivées à partager du code. Toutefois, dans de nombreux cas, 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 méthodes abstraites; il sert de modèle qui définit les membres que chaque classe dérivée doit implémenter. En règle générale, dans une classe de base abstraite, l’implémentation de chaque type dérivé est unique à ce type. Vous avez marqué la classe avec le mot clé abstrait, car il n’a pas été judicieux d’instancier un objet Publication, même si la classe a fourni des implémentations de fonctionnalités communes aux publications.

Par exemple, chaque forme géométrique à deux dimensions fermée comprend deux propriétés : la zone, l’étendue interne de la forme ; et périmètre, ou la distance le long des bords de la forme. Toutefois, la façon dont ces propriétés sont calculées dépend complètement 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. Cela indique que les classes dérivées partagent la même fonctionnalité, mais ces classes dérivées implémentent cette fonctionnalité 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é abstrait, chaque membre d’instance est également marqué avec le mot clé abstrait. Dans ce cas, Shape remplace également la méthode Object.ToString pour retourner le nom du type, plutôt que son nom complet. Il définit deux membres statiques, GetArea et GetPerimeter, qui permettent aux appelants de récupérer facilement la zone 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 certaines classes de Shape qui représentent des formes spécifiques. L’exemple suivant définit trois classes, Square, Rectangleet Circle. Chacune utilise une formule unique pour cette forme particulière pour calculer la zone et le 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 uniques à la forme qu’elles 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 des objets dérivés de Shape. Il instancie un tableau d’objets dérivés de Shape et appelle les méthodes statiques de la classe Shape, qui enveloppent les valeurs des propriétés de retour Shape. Le runtime récupère des valeurs à partir des 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 le cast 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