Ereditarietà - Derivare tipi per creare un comportamento più specializzato

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 di base, mentre la classe che eredita tali membri è denominata classe derivata. Una classe derivata può avere una sola classe di base diretta. L'ereditarietà, tuttavia, è transitiva. Se ClassC è derivato da e ClassB viene derivato da ClassBClassA, 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 da interfacce derivate e da classi che implementano tali interfacce. Per altre informazioni sui metodi di interfaccia predefiniti, 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 nella classe di base senza dover ripetere l'operazione. È possibile aggiungere altri membri nella classe derivata. La classe derivata estende la funzionalità della classe 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 sei membri del proprio. Questi membri includono un costruttore, perché i costruttori non vengono 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;
    }
}

Questo blocco successivo illustra 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 essere il metodo con la propria implementazione. Se una classe di base dichiara un membro come abstract, tale metodo deve essere sottoposto a override in qualsiasi 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 contenere membri astratti; tuttavia, se una classe contiene un membro astratto, la classe stessa deve essere dichiarata come astratta. Le classi derivate che non sono astratte devono fornire l'implementazione per tutti i metodi astratti da una classe di base astratta.

Interfacce

Un'interfaccia è un tipo di 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 "is a". 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 "is a" che esiste tra una classe base e una classe derivata ( ad esempio, un Mammal è un Animal). Per ulteriori informazioni, vedi Interfacce.

Prevenzione di ulteriori derivazione

Una classe può impedire ad altre classi di ereditarlo o da uno dei suoi membri dichiarando se stesso 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 new modificatore può essere usato per indicare in modo esplicito che il membro non deve essere un override del membro di base. L'uso di new non è obbligatorio, ma verrà generato un avviso del compilatore se new non viene usato. Per altre informazioni, vedere Controllo delle versioni con le parole chiave Override e New e Sapere quando usare le parole chiave Override e New.