Leggere in inglese

Condividi tramite


Ereditarietà in C# e .NET

Questa esercitazione presenta l'ereditarietà in C#. L'ereditarietà è una funzionalità dei linguaggi di programmazione orientati agli oggetti che consente di definire una classe base che fornisce funzionalità specifiche (dati e comportamento) e definire classi derivate che ereditano o eseguono l'override di tale funzionalità.

Prerequisiti

  • La versione più recente .NET SDK
  • editor di Visual Studio Code
  • Il DevKit C#

Istruzioni per l'installazione

Su Windows, usa questo file di configurazione WinGet per installare tutti i prerequisiti. Se è già installato un elemento, WinGet ignorerà questo passaggio.

  1. Scaricare il file e fare doppio clic per eseguirlo.
  2. Leggere il contratto di licenza, digitare ye selezionare Immettere quando viene richiesto di accettare.
  3. Se viene visualizzato un prompt di controllo dell'account utente lampeggiante nella barra delle applicazioni, consentire all'installazione di continuare.

In altre piattaforme è necessario installare ognuno di questi componenti separatamente.

  1. Scaricare il programma di installazione consigliato dalla pagina di download di .NET SDK e fare doppio clic per eseguirlo. La pagina di download rileva la piattaforma e consiglia il programma di installazione più recente per la piattaforma.
  2. Scarica il programma di installazione più recente dalla home page Visual Studio Code e fai doppio clic per eseguirlo. Questa pagina rileva anche la tua piattaforma e il collegamento dovrebbe essere corretto per il tuo sistema.
  3. Fare clic sul pulsante "Installa" nella pagina dell'estensione C# DevKit. Verrà aperto Visual Studio Code e viene chiesto se si vuole installare o abilitare l'estensione. Seleziona "install".

Esecuzione degli esempi

Per creare ed eseguire gli esempi in questa esercitazione, si utilizza l'utilità dotnet dalla riga di comando. Seguire questa procedura per ogni esempio:

  1. Creare una directory per archiviare l'esempio.

  2. Immettere il comando dotnet new console al prompt dei comandi per creare un nuovo progetto .NET Core.

  3. Copiare e incollare il codice dall'esempio nell'editor di codice.

  4. Immettere il comando dotnet restore dalla riga di comando per caricare o ripristinare le dipendenze del progetto.

    Non è necessario eseguire dotnet restore perché viene eseguito in modo implicito da tutti i comandi che richiedono un ripristino, ad esempio dotnet new, dotnet build, dotnet run, dotnet test, dotnet publish e dotnet pack. Per disabilitare il ripristino implicito, usare l'opzione --no-restore.

    Il comando dotnet restore è ancora utile in alcuni scenari in cui ha senso eseguire un ripristino esplicito, ad esempio le compilazioni di integrazione continua in Azure DevOps Services o in sistemi di compilazione che richiedono il controllo esplicito quando viene eseguito il ripristino.

    Per informazioni su come gestire i feed NuGet, vedere la dotnet restore documentazione.

  5. Immettere il comando dotnet run per compilare ed eseguire l'esempio.

Background: Che cos'è l'ereditarietà?

l'ereditarietà è uno degli attributi fondamentali della programmazione orientata agli oggetti. Consente di definire una classe figlio che riutilizza (eredita), estende o modifica il comportamento di una classe padre. La classe i cui membri vengono ereditati viene chiamata classe base . La classe che eredita i membri della classe base viene chiamata classe derivata .

C# e .NET supportano solo di ereditarietà singola. Ovvero, una classe può ereditare solo da una singola classe. Tuttavia, l'ereditarietà è transitiva, che consente di definire una gerarchia di ereditarietà per un set di tipi. In altre parole, il tipo D può ereditare dal tipo C, che eredita dal tipo B, che eredita dal tipo di classe base A. Poiché l'ereditarietà è transitiva, i membri di tipo A sono accessibili dal tipo D.

Non tutti i membri di una classe base vengono ereditati dalle classi derivate. I membri seguenti non vengono ereditati:

  • costruttori statici, che inizializzano i dati statici di una classe.

  • costruttori di istanza, che vengono chiamati per creare una nuova istanza della classe. Ogni classe deve definire i propri costruttori.

  • finalizzatori, chiamati dal Garbage Collector del runtime per eliminare definitivamente le istanze di una classe.

Anche se tutti gli altri membri di una classe di base vengono ereditati dalle classi derivate, che siano visibili o meno dipende dall'accessibilità. L'accessibilità di un membro influisce sulla visibilità per le classi derivate come indicato di seguito:

  • membri privati sono visibili solo nelle classi derivate annidate nella relativa classe di base. In caso contrario, non sono visibili nelle classi derivate. Nell'esempio seguente, A.B è una classe annidata che deriva da Ae C deriva da A. Il campo A._value privato è visibile in A.B. Tuttavia, se si rimuovono i commenti dal metodo C.GetValue e si tenta di compilare l'esempio, genera l'errore del compilatore CS0122: "'A._value' è inaccessibile a causa del relativo livello di protezione".

    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
    
  • membri protected sono visibili solo nelle classi derivate.

  • I membri interni sono visibili solo nelle classi derivate che si trovano nella stessa assembly della classe di base. Non sono visibili nelle classi derivate che si trovano in un assembly diverso dalla classe di base.

  • I membri pubblici sono visibili nelle classi derivate e fanno parte dell'interfaccia pubblica delle classi derivate. I membri ereditati pubblici possono essere chiamati esattamente come se siano definiti nella classe derivata. Nell'esempio seguente la classe A definisce un metodo denominato Method1e la classe B eredita dalla classe A. L'esempio chiama quindi Method1 come se fosse un metodo di istanza in 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();
        }
    }
    

Le classi derivate possono anche eseguire l'override membri ereditati fornendo un'implementazione alternativa. Per poter eseguire l'override di un membro, il membro nella classe base deve essere contrassegnato con la parola chiave virtuale. Per impostazione predefinita, i membri della classe base non vengono contrassegnati come virtual e non possono essere sottoposti a override. Il tentativo di eseguire l'override di un membro non virtuale, come nell'esempio seguente, genera l'errore del compilatore CS0506: "<membro> non può eseguire l'override del membro ereditato <membro> perché non è contrassegnato come virtuale, astratto o sottoposto a override".

public class A
{
    public void Method1()
    {
        // Do something.
    }
}

public class B : A
{
    public override void Method1() // Generates CS0506.
    {
        // Do something else.
    }
}

In alcuni casi, una classe derivata deve eseguire l'override dell'implementazione della classe di base. I membri della classe base contrassegnati con la parola chiave astratta richiedono che le classi derivate ne eseggono l'override. Il tentativo di compilare l'esempio seguente genera l'errore del compilatore CS0534, "<classe> non implementa membri astratti ereditati <membro>", perché la classe B non fornisce alcuna implementazione per A.Method1.

public abstract class A
{
    public abstract void Method1();
}

public class B : A // Generates CS0534.
{
    public void Method3()
    {
        // Do something.
    }
}

L'ereditarietà si applica solo a classi e interfacce. Altre categorie di tipi (struct, delegati ed enumerazioni) non supportano l'ereditarietà. A causa di queste regole, il tentativo di compilare codice come l'esempio seguente genera l'errore del compilatore CS0527: "Tipo 'ValueType' nell'elenco di interfacce non è un'interfaccia". Il messaggio di errore indica che, sebbene sia possibile definire le interfacce implementate da uno struct, l'ereditarietà non è supportata.

public struct ValueStructure : ValueType // Generates CS0527.
{
}

Ereditarietà implicita

Oltre a tutti i tipi che possono ereditare da tramite ereditarietà singola, tutti i tipi nel sistema di tipi .NET ereditano in modo implicito da Object o da un tipo derivato da esso. La funzionalità comune di Object è disponibile per ogni tipo.

Per vedere cosa significa l'ereditarietà implicita, definiamo una nuova classe, SimpleClass, che è semplicemente una definizione di classe vuota:

public class SimpleClass
{ }

È quindi possibile usare la reflection (che consente di esaminare i metadati di un tipo per ottenere informazioni su tale tipo) per ottenere un elenco dei membri che appartengono al tipo di SimpleClass. Anche se non sono stati definiti membri nella classe SimpleClass, l'output dell'esempio indica che ha effettivamente nove membri. Uno di questi membri è un costruttore senza parametri (o predefinito) fornito automaticamente per il tipo SimpleClass dal compilatore C#. Gli otto rimanenti sono membri di Object, il tipo da cui ereditano in modo implicito tutte le classi e le interfacce nel sistema dei tipi .NET.

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'ereditarietà implicita dalla classe Object rende disponibili questi metodi alla classe SimpleClass:

  • Il metodo pubblico ToString, che converte un oggetto SimpleClass nella relativa rappresentazione di stringa, restituisce il nome completo del tipo. In questo caso, il metodo ToString restituisce la stringa "SimpleClass".

  • Tre metodi che testano l'uguaglianza di due oggetti: il metodo Equals(Object) dell'istanza pubblica, il metodo Equals(Object, Object) statico pubblico e il metodo ReferenceEquals(Object, Object) statico pubblico. Per impostazione predefinita, questi metodi testano l'uguaglianza dei riferimenti; ovvero, per essere uguale, due variabili oggetto devono fare riferimento allo stesso oggetto.

  • Metodo pubblico GetHashCode, che calcola un valore che consente l'uso di un'istanza del tipo nelle raccolte con hash.

  • Metodo GetType pubblico, che restituisce un oggetto Type che rappresenta il tipo di SimpleClass.

  • Il metodo Finalize protetto, progettato per rilasciare risorse non gestite prima che la memoria di un oggetto venga recuperata dal Garbage Collector.

  • Metodo MemberwiseClone protetto, che crea un clone superficiale dell'oggetto corrente.

A causa dell'ereditarietà implicita, è possibile chiamare qualsiasi membro ereditato da un oggetto SimpleClass come se fosse effettivamente un membro definito nella classe SimpleClass. L'esempio seguente, ad esempio, chiama il metodo SimpleClass.ToString, che SimpleClass eredita da 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

La tabella seguente elenca le categorie di tipi che è possibile creare in C# e i tipi da cui ereditano in modo implicito. Ogni tipo di base rende disponibile un set diverso di membri attraverso l'ereditarietà ai tipi derivati implicitamente.

Categoria di tipi Eredita in modo implicito da
classe Object
struttura ValueType, Object
enum Enum, ValueType, Object
delegato MulticastDelegate, Delegate, Object

Ereditarietà e relazione "è un"

In genere, l'ereditarietà viene usata per esprimere una relazione "è " tra una classe di base e una o più classi derivate, in cui le classi derivate sono versioni specializzate della classe base; la classe derivata è un tipo della classe base. Ad esempio, la classe Publication rappresenta una pubblicazione di qualsiasi tipo e le classi Book e Magazine rappresentano tipi specifici di pubblicazioni.

Nota

Una classe o uno struct può implementare una o più interfacce. Sebbene l'implementazione dell'interfaccia venga spesso presentata come soluzione alternativa per l'ereditarietà singola o come metodo di utilizzo dell'ereditarietà con struct, è progettata per esprimere una relazione diversa (una relazione "può fare") tra un'interfaccia e il relativo tipo di implementazione rispetto all'ereditarietà. Un'interfaccia definisce un sottoinsieme di funzionalità, ad esempio la possibilità di verificare l'uguaglianza, confrontare o ordinare oggetti, o supportare l'analisi e la formattazione sensibili alle impostazioni culturali, che l'interfaccia mette a disposizione dei tipi che la implementano.

Si noti che esprime anche la relazione tra un tipo e un'istanza specifica di tale tipo. Nell'esempio seguente Automobile è una classe con tre proprietà di sola lettura univoche: Make, il produttore dell'automobile; Model, il tipo di automobile; e Year, il suo anno di produzione. La classe Automobile ha anche un costruttore i cui argomenti sono assegnati ai valori della proprietà ed esegue l'override del metodo Object.ToString per produrre una stringa che identifichi in modo univoco l'istanza di Automobile anziché 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}";
}

In questo caso, non dovresti basarti sull'ereditarietà per rappresentare marche e modelli specifici di auto. Ad esempio, non è necessario definire un tipo Packard per rappresentare le automobili prodotte dalla Packard Motor Car Company. È invece possibile rappresentarli creando un oggetto Automobile con i valori appropriati passati al relativo costruttore di classe, come illustrato nell'esempio seguente.

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

Una relazione basata sull'ereditarietà viene applicata in modo ottimale a una classe di base e alle classi derivate che aggiungono membri aggiuntivi alla classe base o che richiedono funzionalità aggiuntive non presenti nella classe base.

Progettazione della classe di base e delle classi derivate

Si esaminerà ora il processo di progettazione di una classe di base e delle relative classi derivate. In questa sezione si definirà una classe base, Publication, che rappresenta una pubblicazione di qualsiasi tipo, ad esempio un libro, una rivista, un giornale, un giornale, un giornale, un articolo e così via. Si definirà anche una classe Book che deriva da Publication. È possibile estendere facilmente l'esempio per definire altre classi derivate, ad esempio Magazine, Journal, Newspapere Article.

Classe base di Pubblicazione

Nella progettazione della classe Publication è necessario prendere diverse decisioni di progettazione:

  • Quali membri includere nella classe di base Publication e se i membri Publication forniscono implementazioni del metodo o se Publication è una classe base astratta che funge da modello per le relative classi derivate.

    In questo caso, la classe Publication fornirà implementazioni del metodo. La sezione Progettazione di classi base astratte e delle relative classi derivate contiene un esempio che usa una classe base astratta per definire i metodi di cui devono eseguire l'override le classi derivate. Le classi derivate sono libere di fornire qualsiasi implementazione adatta al tipo derivato.

    La possibilità di riutilizzare il codice ( ovvero più classi derivate condividono la dichiarazione e l'implementazione dei metodi della classe di base e non devono eseguirne l'override) è un vantaggio delle classi di base non astratte. Pertanto, è consigliabile aggiungere membri a Publication se è probabile che il codice venga condiviso da alcuni o più tipi di Publication specializzati. Se non si forniscono implementazioni di classi di base in modo efficiente, si finisce per fornire implementazioni membro in gran parte identiche nelle classi derivate piuttosto una singola implementazione nella classe base. La necessità di mantenere il codice duplicato in più posizioni è una potenziale origine di bug.

    Sia per ottimizzare il riutilizzo del codice che per creare una gerarchia di ereditarietà logica e intuitiva, è necessario assicurarsi di includere nella classe Publication solo i dati e le funzionalità comuni a tutte o alla maggior parte delle pubblicazioni. Le classi derivate implementano quindi membri univoci per i tipi specifici di pubblicazione rappresentati.

  • Quanto estendere la gerarchia delle classi. Si vuole sviluppare una gerarchia di tre o più classi, anziché semplicemente una classe di base e una o più classi derivate? Ad esempio, Publication potrebbe essere una classe base di Periodical, che a sua volta è una classe base di Magazine, Journal e Newspaper.

    Ad esempio, si userà la piccola gerarchia di una classe Publication e una singola classe derivata, Book. È possibile estendere facilmente l'esempio per creare una serie di classi aggiuntive che derivano da Publication, ad esempio Magazine e Article.

  • Indica se è opportuno creare un'istanza della classe di base. In caso contrario, è necessario applicare la parola chiave astratta alla classe. In caso contrario, è possibile creare un'istanza della classe Publication chiamando il relativo costruttore di classe. Se si tenta di creare un'istanza di una classe contrassegnata con la parola chiave abstract da una chiamata diretta al relativo costruttore di classe, il compilatore C# genera l'errore CS0144, "Cannot create an instance of the abstract class or interface". Se si tenta di creare un'istanza della classe usando la reflection, il metodo di reflection genera un MemberAccessException.

    Per impostazione predefinita, è possibile creare un'istanza di una classe base chiamando il relativo costruttore di classe. Non è necessario definire in modo esplicito un costruttore di classe. Se non è presente nel codice sorgente della classe di base, il compilatore C# fornisce automaticamente un costruttore predefinito (senza parametri).

    Per esempio, si contrassegnerà la classe Publication come astratta in modo tale che non possa essere creata un'istanza. Una classe abstract senza metodi abstract indica che questa classe rappresenta un concetto astratto condiviso tra diverse classi concrete (ad esempio un Book, Journal).

  • Se le classi derivate devono ereditare l'implementazione della classe base di determinati membri, indipendentemente dal fatto che abbiano la possibilità di eseguire l'override dell'implementazione della classe di base o se devono fornire un'implementazione. Si utilizza la parola chiave astratta per forzare le classi derivate a fornire un'implementazione. Usare la parola chiave virtuale per consentire alle classi derivate di eseguire l'override di un metodo della classe base. Per impostazione predefinita, i metodi definiti nella classe base non sono sostituibili.

    La classe Publication non dispone di metodi abstract, ma la classe stessa è abstract.

  • Indica se una classe derivata rappresenta la classe finale nella gerarchia di ereditarietà e non può essere utilizzata come classe di base per classi derivate aggiuntive. Per impostazione predefinita, qualsiasi classe può fungere da classe base. È possibile applicare la parola chiave sealed per indicare che una classe non può fungere da classe di base per altre classi. Tentativo di derivare da una classe sealed ha generato l'errore del compilatore CS0509, "impossibile derivare dal tipo sealed <typeName>".

    Ad esempio, si contrassegnerà la classe derivata come sealed.

Nell'esempio seguente viene illustrato il codice sorgente per la classe Publication, nonché un'enumerazione PublicationType restituita dalla proprietà Publication.PublicationType. Oltre ai membri che eredita da Object, la classe Publication definisce i seguenti membri univoci e gli override dei membri:


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;
}
  • Costruttore

    Poiché la classe Publication è abstract, non è possibile creare un'istanza direttamente dal codice come nell'esempio seguente:

    var publication = new Publication("Tiddlywinks for Experts", "Fun and Games",
                                      PublicationType.Book);
    

    Tuttavia, il costruttore dell'istanza può essere chiamato direttamente dai costruttori di classi derivate, come illustrato dal codice sorgente per la classe Book.

  • Due proprietà correlate alla pubblicazione

    Title è una proprietà di sola lettura String il cui valore viene fornito chiamando il costruttore Publication.

    Pages è una proprietà di Int32 di lettura/scrittura che indica il numero di pagine totali della pubblicazione. Il valore viene archiviato in un campo privato denominato totalPages. Deve essere un numero positivo o viene generata una ArgumentOutOfRangeException.

  • Membri correlati all'editore

    Due proprietà di sola lettura, Publisher e Type. I valori vengono originariamente forniti dalla chiamata al costruttore della classe Publication.

  • Membri legati alla pubblicazione

    Due metodi, Publish e GetPublicationDate, impostare e restituire la data di pubblicazione. Il metodo Publish imposta un flag published privato su true quando viene chiamato e assegna la data passata come argomento al campo datePublished privato. Il metodo GetPublicationDate restituisce la stringa "NYP" se il flag published è falsee il valore del campo datePublished se è true.

  • Membri correlati al copyright

    Il metodo Copyright accetta il nome del titolare del copyright e l'anno del copyright come argomenti e li assegna alle proprietà CopyrightName e CopyrightDate.

  • Sovrascrittura del metodo ToString

    Se un tipo non esegue l'override del metodo Object.ToString, restituisce il nome completo del tipo, che è di poco utile per differenziare un'istanza da un'altra. La classe Publication esegue l'override di Object.ToString per restituire il valore della proprietà Title.

La figura seguente illustra la relazione tra la classe Publication di base e la classe Object ereditata in modo implicito.

Le classi Object e Publication

Classe Book

La classe Book rappresenta un libro come tipo specializzato di pubblicazione. Nell'esempio seguente viene illustrato il codice sorgente per 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}";
}

Oltre ai membri che eredita da Publication, la classe Book definisce i seguenti membri unici e override dei membri:

  • Due costruttori

    I due costruttori Book condividono tre parametri comuni. Due parametri, titolo e editore, corrispondono al costruttore Publication. Il terzo è autore, archiviato in una proprietà Author pubblica non modificabile. Un costruttore include un parametro isbn, memorizzato nella proprietà automatica ISBN.

    Il primo costruttore usa la parola chiave this per richiamare l'altro costruttore. Il concatenamento dei costruttori è un modello comune nella definizione dei costruttori. I costruttori con un minor numero di parametri forniscono valori predefiniti quando si chiama il costruttore con il maggior numero di parametri.

    Il secondo costruttore usa la parola chiave base per passare il titolo e il nome dell'editore al costruttore della classe base. Se non si effettua una chiamata esplicita a un costruttore della classe di base nel codice sorgente, il compilatore C# fornisce automaticamente una chiamata al costruttore predefinito o senza parametri della classe di base.

  • Proprietà di ISBN di sola lettura, che restituisce il numero di libro standard internazionale dell'oggetto Book, un numero univoco di 10 o 13 cifre. Il codice ISBN viene fornito come argomento a uno dei costruttori di Book. Il codice ISBN viene archiviato in un campo sottostante privato, generato automaticamente dal compilatore.

  • Proprietà di Author di sola lettura. Il nome dell'autore viene fornito come argomento ai costruttori Book e viene memorizzato nella proprietà.

  • Due proprietà correlate al prezzo di sola lettura, Price e Currency. I valori vengono forniti come argomenti in una chiamata al metodo SetPrice. La proprietà Currency è il simbolo di valuta ISO a tre cifre, ad esempio USD per il dollaro statunitense. I simboli di valuta ISO possono essere recuperati dalla proprietà ISOCurrencySymbol. Entrambe queste proprietà sono di sola lettura esterna, ma entrambe possono essere impostate dal codice nella classe Book.

  • Metodo SetPrice, che imposta i valori delle proprietà Price e Currency. Quei valori vengono restituiti da quelle stesse proprietà.

  • Esegue l'override del metodo ToString (ereditato da Publication) e dei metodi Object.Equals(Object) e GetHashCode (ereditati da Object).

    A meno che non sia sottoposto a override, il metodo Object.Equals(Object) verifica l'uguaglianza dei riferimenti. Ovvero, due variabili oggetto vengono considerate uguali se fanno riferimento allo stesso oggetto. Nella classe Book, invece, due oggetti Book devono essere uguali se hanno lo stesso ISBN.

    Quando si esegue l'override del metodo Object.Equals(Object), è inoltre necessario eseguire l'override del metodo GetHashCode, che restituisce un valore usato dal runtime per archiviare gli elementi nelle raccolte con hash per un recupero efficiente. Il codice hash deve restituire un valore coerente con il test per verificarne l'uguaglianza. Poiché hai sottoposto a override Object.Equals(Object) per restituire true se le proprietà ISBN di due oggetti Book sono uguali, restituisci il codice hash calcolato chiamando il metodo GetHashCode sulla stringa restituita dalla proprietà ISBN.

Nella figura seguente viene illustrata la relazione tra la classe Book e Publication, la relativa classe di base.

classi di pubblicazioni e di libri

È ora possibile creare un'istanza di un oggetto Book, richiamarne i membri univoci e ereditati e passarlo come argomento a un metodo che prevede un parametro di tipo Publication o di tipo Book, come illustrato nell'esempio seguente.

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

Progettazione di classi base astratte e delle relative classi derivate

Nell'esempio precedente è stata definita una classe base che ha fornito un'implementazione per diversi metodi per consentire alle classi derivate di condividere il codice. In molti casi, tuttavia, la classe base non deve fornire un'implementazione. La classe base è invece una classe astratta che dichiara metodi astratti; funge da modello che definisce i membri che ogni classe derivata deve implementare. In genere in una classe base astratta, l'implementazione di ogni tipo derivato è univoca per tale tipo. Hai contrassegnato la classe con la parola chiave abstract perché non avrebbe avuto senso creare un'istanza di un oggetto Publication, anche se la classe ha fornito implementazioni di funzionalità comuni alle pubblicazioni.

Ad esempio, ogni forma geometrica bidimensionale chiusa include due proprietà: area, l'estensione interna della forma; e perimetrale, o la distanza lungo i bordi della forma. Il modo in cui queste proprietà vengono calcolate, tuttavia, dipende completamente dalla forma specifica. La formula per calcolare il perimetro (o circonferenza) di un cerchio, ad esempio, è diversa da quella di un quadrato. La classe Shape è una classe abstract con metodi abstract. Ciò indica che le classi derivate condividono la stessa funzionalità, ma le classi derivate implementano tale funzionalità in modo diverso.

Nell'esempio seguente viene definita una classe base astratta denominata Shape che definisce due proprietà: Area e Perimeter. Oltre a contrassegnare la classe con la parola chiave astratta, ogni membro dell'istanza viene contrassegnato anche con la parola chiave astratta. In questo caso, Shape esegue anche l'override del metodo Object.ToString per restituire il nome del tipo, anziché il nome completo. Definisce inoltre due membri statici, GetArea e GetPerimeter, che consentono ai chiamanti di recuperare facilmente l'area e il perimetro di un'istanza di qualsiasi classe derivata. Quando si passa un'istanza di una classe derivata a uno di questi metodi, l'override del metodo della classe derivata viene chiamato dal runtime.

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;
}

È quindi possibile derivare alcune classi da Shape che rappresentano forme specifiche. Nell'esempio seguente vengono definite tre classi, Square, Rectanglee Circle. Ognuna usa una formula univoca per la forma specifica per calcolare l'area e il perimetro. Alcune classi derivate definiscono anche proprietà, ad esempio Rectangle.Diagonal e Circle.Diameter, univoche per la forma che rappresentano.

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;
}

Nell'esempio seguente vengono utilizzati oggetti derivati da Shape. Crea un'istanza di una matrice di oggetti derivati da Shape e chiama i metodi statici della classe Shape, che incapsula il ritorno dei valori delle proprietà Shape. Il runtime recupera i valori dalle proprietà sottoposte a override dei tipi derivati. Nell'esempio viene inoltre eseguito il cast di ogni oggetto Shape nella matrice al tipo derivato e, se il cast ha esito positivo, recupera le proprietà di tale particolare sottoclasse di 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