Ereditarietà in C# e .NET
Questa esercitazione presenta l'ereditarietà in C#. L'ereditarietà è una caratteristica dei linguaggi di programmazione orientati a oggetti che consente di definire una classe di base con funzionalità specifiche (relative a dati e comportamento) e classi derivate che ereditano o eseguono l'override di tali funzionalità.
Prerequisiti
- È consigliabile usare Visual Studio per Windows. È possibile scaricare una versione gratuita dalla pagina di download di Visual Studio. Visual Studio include la SDK .NET.
- È possibile utilizzare anche l'editor di Visual Studio Code con il DevKit C#. Sarà necessario installare separatamente la versione più recente di SDK .NET.
- Se si preferisce un editor diverso, è necessario installare la versione più recente di SDK .NET.
Esecuzione degli esempi
Per creare ed eseguire gli esempi in questa esercitazione, viene usata l'utilità dotnet dalla riga di comando. Eseguire questi passaggi per ogni esempio:
Creare una directory per archiviare l'esempio.
Per creare un nuovo progetto.NET Core, immettere il comando dotnet new console al prompt dei comandi.
Copiare e incollare il codice dell'esempio nell'editor di codice.
Per caricare o ripristinare le dipendenze del progetto, eseguire il comando dotnet restore dalla riga di comando.
Non è necessario eseguire
dotnet restore
perché viene eseguito in modo implicito da tutti i comandi che richiedono un ripristino, ad esempiodotnet new
,dotnet build
,dotnet run
,dotnet test
,dotnet publish
edotnet 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.Per compilare ed eseguire l'esempio, immettere il comando dotnet run.
Informazioni generali: che cos'è l'ereditarietà?
Il concetto di ereditarietà è uno degli attributi fondamentali della programmazione orientata a oggetti. L'ereditarietà 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 è denominata classe di base. Quella che eredita i membri della classe di base è denominata classe derivata.
C# e .NET supportano solo l'ereditarietà singola. Ciò significa che una classe può solo ereditare da una singola classe. L'ereditarietà tuttavia è transitiva, pertanto è possibile 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
, il quale eredita a sua volta dal tipo della classe di base A
. Poiché l'ereditarietà è transitiva, i membri del tipo A
sono disponibili per il tipo D
.
Non tutti i membri di una classe di base vengono ereditati dalle classi derivate. I membri seguenti non vengono ereditati:
Costruttori statici, che inizializzano i dati statici di una classe.
Costruttori di istanze, che vengono chiamati per creare una nuova istanza della classe. Ogni classe deve definire propri costruttori.
Finalizzatori, che vengono chiamati dal Garbage Collector di runtime per distruggere le istanze di una classe.
Tutti gli altri membri di una classe di base vengono ereditati dalle classi derivate, ma la loro visibilità dipende dall'accessibilità. L'accessibilità di un membro ne determina la visibilità per le classi derivate, come indicato di seguito:
I membri privati sono visibili solo nelle classi derivate che sono 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 daA
eC
deriva daA
. Il campo privatoA._value
è visibile in A.B. Se tuttavia si rimuovono i commenti dal metodoC.GetValue
e si tenta di compilare l'esempio, verrà generato l'errore del compilatore CS0122: "'A.value' non è accessibile a causa del livello di protezione impostato".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
I membri protetti sono visibili solo nelle classi derivate.
I membri interni sono visibili solo nelle classi derivate che si trovano nello stesso 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 della classe derivata. I membri pubblici ereditati possono essere chiamati come se fossero definiti nella classe derivata. Nell'esempio seguente la classe
A
definisce un metodo denominatoMethod1
e la classeB
eredita dalla classeA
. Nell'esempio viene quindi chiamatoMethod1
come se fosse un metodo di istanza inB
.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 dei membri ereditati fornendo un'implementazione alternativa. Per poter eseguire l'override di un membro, il membro nella classe di base deve essere contrassegnato con la parola chiave virtual. Per impostazione predefinita, i membri della classe di base non sono contrassegnati come virtual
e non possono essere sottoposti a override. Se si prova a eseguire l'override di un membro non virtuale, come in questo esempio, viene generato l'errore del compilatore CS0506: "<member>: impossibile eseguire l'override del membro ereditato <member> perché non è contrassegnato come virtual, abstract o 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 di base contrassegnati con la parola chiave abstract richiedono di essere sottoposti a override dalle classi derivate. Se si prova a compilare l'esempio seguente, verrà generato l'errore del compilatore CS0534, "<classe> non implementa il membro astratto <membro> ereditato", 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 alle classi e alle interfacce. Le altre categorie di tipi (struct, delegati ed enumerazioni) non supportano l'ereditarietà. Per queste regole, se si prova a compilare il codice come nell'esempio seguente, verrà generato l'errore del compilatore CS0527: "Il tipo 'ValueType' nell'elenco delle interfacce non è un'interfaccia". Il messaggio di errore indica che, sebbene sia possibile definire le interfacce implementate da un tipo struct, l'ereditarietà non è supportata.
public struct ValueStructure : ValueType // Generates CS0527.
{
}
Ereditarietà implicita
Oltre ai tipi da cui possono ereditare tramite l'ereditarietà singola, tutti i tipi nel sistema di tipi .NET ereditano in modo implicito da Object o da un tipo derivato. Le funzionalità comuni di Object saranno disponibili per qualsiasi tipo.
Per comprendere il significato dell'ereditarietà implicita, si definirà 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 del tipo per ottenere informazioni su di esso, per generare un elenco dei membri che appartengono al tipo SimpleClass
. Anche se non è stato definito alcun membro nella classe SimpleClass
, l'output dell'esempio indica che ha effettivamente nove membri. Uno di questi è un costruttore senza parametri (o predefinito) fornito automaticamente per il tipo SimpleClass
dal compilatore C#. I rimanenti otto sono membri di Object, il tipo da cui ereditano in modo implicito tutte le classi e le interfacce nel sistema di 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 per la classe SimpleClass
:
Il metodo pubblico
ToString
, che converte un oggettoSimpleClass
nella relativa rappresentazione stringa, restituisce il nome di tipo completo. In questo caso il metodoToString
restituisce la stringa "SimpleClass".Tre metodi che verificano l'uguaglianza di due oggetti: il metodo pubblico di istanza
Equals(Object)
, il metodo statico pubblicoEquals(Object, Object)
e il metodo statico pubblicoReferenceEquals(Object, Object)
. Per impostazione predefinita, questi metodi verificano l'uguaglianza dei riferimenti. Ciò significa che, per essere uguali, due variabili di oggetto devono fare riferimento allo stesso oggetto.Il metodo pubblico
GetHashCode
, che calcola un valore che consente di usare un'istanza del tipo nelle raccolte con hash.Il metodo pubblico
GetType
, che restituisce un oggetto Type che rappresenta il tipoSimpleClass
.Il metodo protetto Finalize, che è progettato per rilasciare le risorse non gestite prima che la memoria di un oggetto venga recuperata dal Garbage Collector.
Il metodo protetto MemberwiseClone, che crea un clone superficiale dell'oggetto corrente.
Grazie all'ereditarietà implicita, è possibile chiamare qualsiasi membro ereditato da un oggetto SimpleClass
come se fosse effettivamente un membro definito nella classe SimpleClass
. Nell'esempio seguente viene chiamato 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
Nella tabella seguente sono elencate le categorie di tipi che è possibile creare in C# e i tipi da cui ereditano in modo implicito. Tramite l'ereditarietà ciascun tipo di base rende disponibile un set di membri diverso per i tipi derivati in modo implicito.
Categoria di tipi | Eredita in modo implicito da |
---|---|
class | Object |
struct | ValueType, Object |
enum | Enum, ValueType, Object |
delegate | MulticastDelegate, Delegate, Object |
Ereditarietà e relazione "è un"
In genere l'ereditarietà consente di esprimere una relazione "è un" tra una classe di base e una o più classi derivate, in cui le classi derivate sono versioni specializzate della classe di base. La classe derivata è un tipo della classe di base. La classe Publication
rappresenta ad esempio 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. Anche se l'implementazione dell'interfaccia è spesso presentata come una soluzione alternativa all'ereditarietà singola o come modo per usare l'ereditarietà con struct, è stata ideata 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 subset di funzionalità che rende disponibili per i tipi di implementazione, ad esempio le funzionalità per verificare l'uguaglianza, confrontare o ordinare gli oggetti, nonché supportare la formattazione e l'analisi in base alle impostazioni cultura.
Si noti che "è un" esprime anche la relazione tra un tipo e un'istanza specifica di quel tipo. Nell'esempio seguente Automobile
è una classe che ha tre proprietà univoche di sola lettura: Make
, il produttore dell'automobile, Model
, il tipo di automobile e Year
, l'anno di produzione. La classe Automobile
include anche un costruttore i cui argomenti vengono assegnati ai valori delle proprietà ed esegue l'override del metodo Object.ToString per generare una stringa che identifica in modo univoco l'istanza 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 è opportuno non fare affidamento sull'ereditarietà per rappresentare marche e modelli specifici di automobili. Non è ad esempio necessario definire un tipo Packard
che rappresenta automobili prodotte dalla casa automobilistica Packard. È invece possibile rappresentarle creando un oggetto Automobile
con i valori appropriati passati al costruttore della 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
È preferibile applicare una relazione "è un" basata sull'ereditarietà a una classe di base e a classi derivate che aggiungono altri membri alla classe di base o che richiedono funzionalità aggiuntive non presenti nella classe di base.
Progettazione della classe di base e delle classi derivate
Si esaminerà ora il processo di progettazione della classe di base e delle relative classi derivate. In questa sezione si definirà una classe di base, Publication
, che rappresenta una pubblicazione di qualsiasi tipo, ad esempio un libro, una rivista, un giornale, un diario, un articolo e così via. Si definirà anche una classe Book
che deriva da Publication
. L'esempio può essere facilmente esteso alla definizione di altre classi derivate, ad esempio Magazine
, Journal
, Newspaper
e Article
.
Classe di base Publication
Per progettare la classe Publication
, è necessario prendere alcune decisioni di progettazione:
Quali membri includere nella classe di base
Publication
e se i membriPublication
forniscono le implementazioni del metodo o sePublication
è una classe di base astratta che funge da modello per le relative classi derivate.In questo caso la classe
Publication
fornirà le implementazioni del metodo. La sezione Progettazione di classi di base astratte e delle relative classi derivate contiene un esempio in cui viene usata una classe base astratta per definire i metodi di cui le classi derivate devono eseguire l'override. Le classi derivate possono fornire qualsiasi implementazione adatta al tipo derivato.La possibilità di riutilizzare il codice, ovvero il fatto che più classi derivate condividano la dichiarazione e l'implementazione dei metodi della classe di base e non ne richiedano l'override, è un vantaggio delle classi di base non astratte. È quindi necessario aggiungere membri a
Publication
se è probabile che il relativo codice venga condiviso da alcuni o dalla maggior parte dei tipi specializzatiPublication
. Se le implementazioni delle classi di base non vengono eseguite in modo efficiente, sarà necessario fornire implementazioni di membri pressoché identiche nelle classi derivate anziché una singola implementazione nella classe di base. La necessità di mantenere il codice duplicato in più posizioni è una potenziale fonte di bug.Per ottimizzare il riutilizzo del codice e per creare una gerarchia di ereditarietà logica e intuitiva, è opportuno assicurarsi che nella classe
Publication
vengano inclusi solo i dati e le funzionalità comuni a tutte le pubblicazioni o alla maggior parte di esse. Le classi derivate implementano quindi i membri che sono univoci per i tipi di pubblicazione specifici che rappresentano.Fino a che punto estendere la gerarchia di classi. È necessario decidere se si vuole sviluppare una gerarchia di tre o più classi, anziché semplicemente una classe di base e una o più classi derivate.
Publication
può ad esempio essere una classe di base diPeriodical
, che a sua volta è una classe di base diMagazine
,Journal
eNewspaper
.Per questo esempio si userà la piccola gerarchia di una classe
Publication
e di una singola classe derivataBook
. L'esempio può essere facilmente esteso alla creazione di una serie di classi aggiuntive che derivano daPublication
, ad esempioMagazine
eArticle
.Se è opportuno creare un'istanza della classe di base. In caso contrario, è necessario applicare alla classe la parola chiave abstract. Altrimenti è possibile creare un'istanza della classe
Publication
chiamando il relativo costruttore di classe. Se si prova a creare un'istanza di una classe contrassegnata con la parola chiaveabstract
da una chiamata diretta al costruttore della classe, il compilatore C# genera l'errore CS0144, "Non è possibile creare un'istanza della classe o dell'interfaccia astratta". Se si prova a creare un'istanza della classe usando la reflection, il metodo di reflection genera un'eccezione MemberAccessException.Per impostazione predefinita, è possibile creare un'istanza della classe di base chiamando il relativo costruttore di classe. Non è necessario definire in modo esplicito un costruttore di classe. Se non è presente un costruttore nel codice sorgente della classe di base, il compilatore C# ne fornisce automaticamente uno predefinito (senza parametri).
Per questo esempio, la classe
Publication
verrà contrassegnata come abstract in modo che non sia possibile crearne un'istanza. Una classeabstract
senza metodiabstract
indica che questa classe rappresenta un concetto astratto condiviso tra diverse classi concrete (ad esempioBook
,Journal
).Se le classi derivate devono ereditare l'implementazione della classe di base di un membro specifico, o se possono eseguire l'override dell'implementazione della classe di base, o ancora se devono fornire un'implementazione. La parola chiave abstract si usa per forzare le classi derivate a fornire un'implementazione. Usare la parola chiave virtual per consentire alle classi derivate di eseguire l'override di un metodo della classe di base. Per impostazione predefinita, non è possibile eseguire l'override dei metodi definiti nella classe di base.
La classe
Publication
non ha metodiabstract
, ma la classe stessa èabstract
.Se una classe derivata rappresenta la classe finale nella gerarchia di ereditarietà e non può essere usata come classe di base per altre classi derivate. Per impostazione predefinita, qualsiasi classe può essere usata come classe di base. È possibile applicare la parola chiave sealed per indicare che una classe non può essere usata come classe di base per altre classi. Il tentativo di derivazione da una classe sealed genera l'errore del compilatore CS0509, "non può derivare dal tipo sealed <typeName>".
Per esempio, contrassegnare la classe derivata come
sealed
.
L'esempio seguente illustra il codice sorgente della classe Publication
, nonché un'enumerazione PublicationType
restituita dalla proprietà Publication.PublicationType
. Oltre ai membri che eredita da Object, la classe Publication
definisce i membri univoci e gli override dei membri seguenti:
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 crearne un'istanza direttamente dal codice, come nell'esempio seguente:var publication = new Publication("Tiddlywinks for Experts", "Fun and Games", PublicationType.Book);
Il relativo costruttore di istanze può essere tuttavia chiamato direttamente dai costruttori delle classi derivate, come illustrato dal codice sorgente della classe
Book
.Due proprietà relative alla pubblicazione
Title
è una proprietà String di sola lettura il cui valore viene fornito chiamando il costruttorePublication
.Pages
è una proprietà Int32 di lettura/scrittura che indica il numero totale di pagine contenute nella pubblicazione. Il valore viene archiviato in un campo privato denominatototalPages
. Deve essere un numero positivo, altrimenti viene generata un'eccezione ArgumentOutOfRangeException.Membri relativi all'editore
Due proprietà di sola lettura,
Publisher
eType
. I valori sono forniti in origine tramite la chiamata al costruttore di classePublication
.Membri relativi alla pubblicazione
Due metodi,
Publish
eGetPublicationDate
, impostano e restituiscono la data di pubblicazione. Il metodoPublish
imposta un flag privatopublished
sutrue
quando viene chiamato e assegna la data passata come argomento al campo privatodatePublished
. Il metodoGetPublicationDate
restituisce la stringa "NYP" se il flagpublished
èfalse
e il valore del campodatePublished
se ètrue
.Membri relativi al copyright
Il metodo
Copyright
accetta come argomenti il nome del titolare del copyright e l'anno del copyright e li assegna alle proprietàCopyrightName
eCopyrightDate
.Override del metodo
ToString
Se un tipo non esegue l'override del metodo Object.ToString, restituisce il nome completo del tipo, che è pressoché inutile per distinguere 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 di base Publication
e la relativa classe Object ereditata in modo implicito.
Classe Book
La classe Book
rappresenta un libro come tipo specializzato di pubblicazione. L'esempio seguente illustra il codice sorgente della 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 membri univoci e gli override dei membri seguenti:
Due costruttori
I due costruttori
Book
condividono tre parametri comuni. Due, title e publisher, corrispondono ai parametri del costruttorePublication
. Il terzo è author, che viene archiviato in una proprietàAuthor
pubblica non modificabile. Un costruttore include un parametro isbn, che viene archiviato nella proprietà automaticaISBN
.Il primo costruttore usa la parola chiave this per chiamare l'altro costruttore. Il concatenamento di costruttori è un modello comune nella definizione dei costruttori. I costruttori con meno parametri forniscono i valori predefiniti quando chiamano 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 di base. Se non si esegue una chiamata esplicita a un costruttore della classe di base nel codice sorgente, il compilatore C# effettua automaticamente una chiamata al costruttore della classe di base predefinito o senza parametri.
Una proprietà
ISBN
di sola lettura, che restituisce il numero ISBN (International Standard Book Number) dell'oggettoBook
, un numero univoco a 10 o 13 cifre. Il numero ISBN viene fornito come argomento a uno dei costruttoriBook
. Il numero ISBN viene archiviato in un campo sottostante privato, che viene generato automaticamente dal compilatore.Una proprietà
Author
di sola lettura. Il nome dell'autore viene fornito come argomento a entrambi i costruttoriBook
e viene archiviato nella proprietà.Due proprietà di sola lettura relative ai prezzi,
Price
eCurrency
. I relativi valori vengono forniti come argomenti in una chiamata al metodoSetPrice
. 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 esternamente di sola lettura, ma possono essere entrambe impostate dal codice nella classeBook
.Un metodo
SetPrice
, che imposta i valori delle proprietàPrice
eCurrency
. Questi valori vengono restituiti dalle stesse proprietà.Esegue l'override del metodo
ToString
ereditato daPublication
e dei metodi Object.Equals(Object) e GetHashCode (ereditati da Object).A meno che non venga sottoposto a override, il metodo Object.Equals(Object) verifica l'uguaglianza dei riferimenti. Ciò significa che due variabili di oggetto sono considerate uguali se fanno riferimento allo stesso oggetto. Nella classe
Book
, d'altra parte, due oggettiBook
sono considerati uguali se hanno lo stesso ISBN.Quando si esegue l'override del metodo Object.Equals(Object), è necessario eseguire l'override anche del metodo GetHashCode, che restituisce un valore che verrà usato dal runtime per archiviare elementi in raccolte con hash e facilitarne così il recupero. Il codice hash deve restituire un valore coerente con il test di uguaglianza. Poiché Object.Equals(Object) è stato sottoposto a override per restituire
true
se le proprietà ISBN di due oggettiBook
sono uguali, si restituisce il codice hash calcolato chiamando il metodo GetHashCode della stringa restituita dalla proprietàISBN
.
La figura seguente illustra la relazione tra la classe Book
e la relativa classe di base Publication
.
È ora possibile creare un'istanza di un oggetto Book
, richiamarne i membri univoci ed ereditati e passarla come argomento a un metodo che prevede un parametro di tipo Publication
o 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 di base astratte e delle relative classi derivate
Nell'esempio precedente si è definita una classe di base che fornisce un'implementazione per una serie di metodi per consentire alle classi derivate di condividere il codice. In molti casi, tuttavia, la classe di base non deve fornire un'implementazione. Al contrario, è una classe astratta che dichiara dei metodi astratti e funge da modello che definisce i membri che ogni classe derivata deve implementare. Per una classe di base astratta l'implementazione di ogni tipo derivato è in genere univoca per quel tipo. La classe è stata contrassegnata con la parola chiave abstract perché non si è ritenuto logico creare un'istanza di un oggetto Publication
, anche se la classe ha fornito implementazioni di funzionalità comuni alle pubblicazioni.
Ogni forma geometrica bidimensionale chiusa include ad esempio due proprietà: l'area, l'estensione interna della forma e il perimetro, ovvero la lunghezza totale dei bordi della forma. La modalità di calcolo di queste proprietà dipende tuttavia completamente dalla forma specifica. La formula per calcolare il perimetro (o circonferenza) di un cerchio è ad esempio diversa da quella usata per un quadrato. La classe Shape
è una classe abstract
con i metodi abstract
. Ciò indica che le classi derivate condividono la stessa funzionalità, ma implementano questa funzionalità in modo diverso.
L'esempio seguente definisce una classe di base astratta denominata Shape
che definisce due proprietà: Area
e Perimeter
. Oltre a contrassegnare la classe con la parola chiave abstract, si contrassegna con la parola chiave abstract anche ogni membro dell'istanza. 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, il runtime chiama l'override del metodo della classe derivata.
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 da Shape
alcune classi che rappresentano forme specifiche. Nell'esempio seguente vengono definite tre classi, Square
, Rectangle
e Circle
. Ogni classe usa una formula univoca per calcolare l'area e il perimetro della forma specifica. Alcune classi derivate definiscono anche le proprietà, ad esempio Rectangle.Diagonal
e Circle.Diameter
, che sono 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 usati gli oggetti derivati da Shape
. Viene creata un'istanza di una matrice di oggetti derivati da Shape
e vengono chiamati i metodi statici della classe Shape
, che esegue il wrapping dei valori restituiti della proprietà Shape
. Il runtime recupera i valori dalle proprietà dei tipi derivati sottoposte a override. Nell'esempio viene anche eseguito il cast di ogni oggetto Shape
nella matrice al relativo tipo derivato e, se il cast ha esito positivo, vengono recuperate le proprietà di quella sottoclasse specifica 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