Ereditarietà: derivare i tipi per creare comportamenti più specializzati

L'ereditarietà, insieme all'incapsulamento e al polimorfismo, rappresenta una delle tre principali caratteristiche (o pilastri) della programmazione orientata a oggetti. L'ereditarietà consente di creare nuove classi che riutilizzano, estendono e modificano il comportamento definito in altre classi. La classe i cui membri vengono ereditati è denominata classe base, mentre la classe che eredita i membri è denominata classe derivata. Una classe derivata può avere una sola classe di base diretta. L'ereditarietà, tuttavia, è transitiva. Se ClassC è derivato da ClassBe ClassB è derivato da ClassA, ClassC eredita i membri dichiarati in ClassB e ClassA.

Nota

Gli struct non supportano l'ereditarietà, mentre possono implementare interfacce.

Concettualmente, una classe derivata rappresenta una specializzazione della classe di base. Ad esempio, avendo una classe di base Animal, è possibile definire una classe derivata denominata Mammal e un'altra classe derivata denominata Reptile. Un oggetto Mammal è anche un oggetto Animal e un oggetto Reptile è anche un Animal, ma ogni classe derivata rappresenta una diversa specializzazione della classe di base.

Le dichiarazioni di interfaccia possono definire un'implementazione predefinita per i relativi membri. Queste implementazioni vengono ereditate dalle interfacce derivate e dalle classi che implementano tali interfacce. Per altre informazioni sui metodi predefiniti delle interfacce, vedere l'articolo sulle interfacce.

Quando si definisce una classe derivandola da un'altra classe, la classe derivata acquista implicitamente tutti i membri della classe di base, con l'eccezione dei costruttori e dei finalizzatori. La classe derivata riutilizza il codice della classe di base senza doverlo implementare nuovamente. È possibile aggiungere altri membri nella classe derivata. La classe derivata estende le funzionalità della classe di base.

La figura riportata di seguito illustra una classe WorkItem che rappresenta un elemento di lavoro in un qualche processo aziendale. Come per tutte le classi, è derivata da System.Object ed eredita tutti i metodi di tale classe. WorkItem aggiunge cinque propri membri. Questi membri includono un costruttore, perché i costruttori non sono ereditati. La classe ChangeRequest eredita da WorkItem e rappresenta un particolare tipo di elemento di lavoro. ChangeRequest aggiunge altri due membri ai membri che eredita da WorkItem e da Object. Deve aggiungere il proprio costruttore e aggiunge anche originalItemID. La proprietà originalItemID consente l'associazione dell'istanza di ChangeRequest all'oggetto WorkItem originale a cui si applica la richiesta di modifica.

Diagram that shows class inheritance

Nell'esempio seguente viene illustrato come le relazioni tra le classi mostrate nella precedente illustrazione vengono espresse in C#. Nell'esempio viene descritto anche come WorkItem esegue l'override del metodo virtuale Object.ToString e come la classe ChangeRequest eredita l'implementazione del metodo propria della classe WorkItem. Il primo blocco definisce le classi:

// WorkItem implicitly inherits from the Object class.
public class WorkItem
{
    // Static field currentID stores the job ID of the last WorkItem that
    // has been created.
    private static int currentID;

    //Properties.
    protected int ID { get; set; }
    protected string Title { get; set; }
    protected string Description { get; set; }
    protected TimeSpan jobLength { get; set; }

    // Default constructor. If a derived class does not invoke a base-
    // class constructor explicitly, the default constructor is called
    // implicitly.
    public WorkItem()
    {
        ID = 0;
        Title = "Default title";
        Description = "Default description.";
        jobLength = new TimeSpan();
    }

    // Instance constructor that has three parameters.
    public WorkItem(string title, string desc, TimeSpan joblen)
    {
        this.ID = GetNextID();
        this.Title = title;
        this.Description = desc;
        this.jobLength = joblen;
    }

    // Static constructor to initialize the static member, currentID. This
    // constructor is called one time, automatically, before any instance
    // of WorkItem or ChangeRequest is created, or currentID is referenced.
    static WorkItem() => currentID = 0;

    // currentID is a static field. It is incremented each time a new
    // instance of WorkItem is created.
    protected int GetNextID() => ++currentID;

    // Method Update enables you to update the title and job length of an
    // existing WorkItem object.
    public void Update(string title, TimeSpan joblen)
    {
        this.Title = title;
        this.jobLength = joblen;
    }

    // Virtual method override of the ToString method that is inherited
    // from System.Object.
    public override string ToString() =>
        $"{this.ID} - {this.Title}";
}

// ChangeRequest derives from WorkItem and adds a property (originalItemID)
// and two constructors.
public class ChangeRequest : WorkItem
{
    protected int originalItemID { get; set; }

    // Constructors. Because neither constructor calls a base-class
    // constructor explicitly, the default constructor in the base class
    // is called implicitly. The base class must contain a default
    // constructor.

    // Default constructor for the derived class.
    public ChangeRequest() { }

    // Instance constructor that has four parameters.
    public ChangeRequest(string title, string desc, TimeSpan jobLen,
                         int originalID)
    {
        // The following properties and the GetNexID method are inherited
        // from WorkItem.
        this.ID = GetNextID();
        this.Title = title;
        this.Description = desc;
        this.jobLength = jobLen;

        // Property originalItemID is a member of ChangeRequest, but not
        // of WorkItem.
        this.originalItemID = originalID;
    }
}

Il blocco successivo mostra come usare le classi di base e derivate:

// Create an instance of WorkItem by using the constructor in the
// base class that takes three arguments.
WorkItem item = new WorkItem("Fix Bugs",
                            "Fix all bugs in my code branch",
                            new TimeSpan(3, 4, 0, 0));

// Create an instance of ChangeRequest by using the constructor in
// the derived class that takes four arguments.
ChangeRequest change = new ChangeRequest("Change Base Class Design",
                                        "Add members to the class",
                                        new TimeSpan(4, 0, 0),
                                        1);

// Use the ToString method defined in WorkItem.
Console.WriteLine(item.ToString());

// Use the inherited Update method to change the title of the
// ChangeRequest object.
change.Update("Change the Design of the Base Class",
    new TimeSpan(4, 0, 0));

// ChangeRequest inherits WorkItem's override of ToString.
Console.WriteLine(change.ToString());
/* Output:
    1 - Fix Bugs
    2 - Change the Design of the Base Class
*/

Metodi astratti e virtuali

Quando una classe di base dichiara un metodo come virtual, una classe derivata può override il metodo con la propria implementazione. Se una classe di base dichiara un membro come abstract, tale metodo deve essere sottoposto a override in ogni classe non astratta che eredita direttamente da tale classe. Quando una classe derivata è essa stessa astratta, eredita i membri astratti senza implementarli. I membri astratti e virtuali costituiscono la base del polimorfismo, che rappresenta la seconda principale caratteristica della programmazione orientata a oggetti. Per altre informazioni, vedere Polimorfismo.

Classi di base astratte

Se si vuole evitare la generazione di istanze dirette di una classe, è possibile dichiarare una classe come abstract usando l'operatore new. Una classe astratta può essere usata solo se una nuova classe è derivata da essa. Una classe astratta può contenere una o più firme di metodi, a loro volta dichiarate come astratte. Tali firme specificano i parametri e il valore restituito, ma non definiscono alcuna implementazione (corpo del metodo). Una classe astratta non deve necessariamente contenere membri astratti. Tuttavia, se una classe contiene un membro astratto, la classe stessa deve essere dichiarata astratta. Le classi derivate che non sono astratte devono fornire l'implementazione di ogni metodo astratto da una classe di base astratta.

Interfacce

Un'interfaccia è un tipo riferimento che definisce un set di membri. Tutte le classi e gli struct che implementano tale interfaccia devono implementare tale set di membri. Un'interfaccia può definire un'implementazione predefinita per uno o tutti questi membri. Una classe può implementare più interfacce, anche se può essere derivata solo da una singola classe di base diretta.

Le interfacce vengono usate per definire funzionalità specifiche per le classi che non hanno necessariamente una relazione "è un". Ad esempio, l'interfaccia System.IEquatable<T> può essere implementata da qualsiasi classe o struct per determinare se due oggetti del tipo sono equivalenti (tuttavia il tipo definisce l'equivalenza). IEquatable<T> non implica lo stesso tipo di relazione "è " presente tra una classe di base e una classe derivata ( ad esempio, un Mammal è un Animal). Per altre informazioni, vedere Interfacce.

Prevenzione di un'ulteriore derivazione

È possibile evitare che altre classi ereditino da una data classe o da uno qualsiasi dei suoi membri, dichiarando se stessa o il membro come sealed.

Classe derivata che nasconde i membri della classe di base

Una classe derivata può nascondere i membri di una classe di base dichiarando dei membri con lo stesso nome e la stessa firma. Il modificatore new può essere usato per indicare in modo esplicito che il membro non costituisce un override del membro di base. L'uso di new non è obbligatorio, ma viene generato un avviso del compilatore se non viene usato new. Per altre informazioni, vedere Controllo delle versioni con le parole chiave Override e New e Sapere quando usare le parole chiave Override e New.