Polimorfismo

Il polimorfismo è spesso definito il terzo pilastro della programmazione orientata a oggetti, dopo l'incapsulamento e l'ereditarietà. Polimorfismo è una parola che deriva dal greco e significa "multiforme". Il polimorfismo presenta due aspetti distinti:

  • In fase di esecuzione, oggetti di una classe derivata possono essere trattati come oggetti di una classe base in posizioni quali parametri del metodo e raccolte o matrici. Quando si verifica questo polimorfismo, il tipo dichiarato dell'oggetto non è più identico al tipo in fase di esecuzione.
  • Le classi base possono definire e implementare metodi virtuali e le classi derivate possono eseguirne l'override, ovvero ne forniscono una propria definizione e implementazione. Durante la fase di esecuzione, quando il codice client chiama il metodo, CLR cerca il tipo in fase di esecuzione dell'oggetto e richiama quell'override del metodo virtuale. Nel codice sorgente è possibile chiamare un metodo su una classe di base e causare l'esecuzione della versione di una classe derivata del metodo.

I metodi virtuali consentono di usare gruppi di oggetti correlati in modo uniforme. Si supponga ad esempio di avere un'applicazione di disegno che consenta a un utente di creare vari tipi di forme in un'area di disegno. In fase di compilazione non è possibile sapere quali tipi specifici di forme creerà l'utente. L'applicazione deve tuttavia tenere traccia di tutti i vari tipi di forme create e deve aggiornarli in risposta alle azioni del mouse dell'utente. È possibile usare il polimorfismo per risolvere questo problema in due passaggi di base:

  1. Creare una gerarchia di classi nella quale ogni classe della forma specifica deriva da una classe base comune.
  2. Usare un metodo virtuale per richiamare il metodo adatto su qualsiasi classe derivata tramite una sola chiamata al metodo della classe base.

Prima di tutto, creare una classe base denominata Shape e delle classi derivate quali Rectangle, Circle e Triangle. Definire nella classe Shape un metodo virtuale denominato Draw ed eseguirne l'override in ogni classe derivata per disegnare la particolare forma che la classe rappresenta. Creare un oggetto List<Shape> e aggiungervi un oggetto Circle, Triangle e Rectangle.

public class Shape
{
    // A few example members
    public int X { get; private set; }
    public int Y { get; private set; }
    public int Height { get; set; }
    public int Width { get; set; }

    // Virtual method
    public virtual void Draw()
    {
        Console.WriteLine("Performing base class drawing tasks");
    }
}

public class Circle : Shape
{
    public override void Draw()
    {
        // Code to draw a circle...
        Console.WriteLine("Drawing a circle");
        base.Draw();
    }
}
public class Rectangle : Shape
{
    public override void Draw()
    {
        // Code to draw a rectangle...
        Console.WriteLine("Drawing a rectangle");
        base.Draw();
    }
}
public class Triangle : Shape
{
    public override void Draw()
    {
        // Code to draw a triangle...
        Console.WriteLine("Drawing a triangle");
        base.Draw();
    }
}

Per aggiornare l'area di disegno, usare un ciclo foreach per scorrere l'elenco e chiamare il metodo Draw su ogni oggetto Shape nell'elenco. Anche se ogni oggetto nella lista ha un tipo dichiarato di Shape, sarà il tipo della fase di esecuzione (la versione del metodo di cui è stato eseguito l'override in ogni classe derivata) che verrà richiamato.

// Polymorphism at work #1: a Rectangle, Triangle and Circle
// can all be used wherever a Shape is expected. No cast is
// required because an implicit conversion exists from a derived
// class to its base class.
var shapes = new List<Shape>
{
    new Rectangle(),
    new Triangle(),
    new Circle()
};

// Polymorphism at work #2: the virtual method Draw is
// invoked on each of the derived classes, not the base class.
foreach (var shape in shapes)
{
    shape.Draw();
}
/* Output:
    Drawing a rectangle
    Performing base class drawing tasks
    Drawing a triangle
    Performing base class drawing tasks
    Drawing a circle
    Performing base class drawing tasks
*/

In C# ogni tipo è polimorfico perché tutti i tipi, incluso i tipi definiti dall'utente, ereditano da Object.

Panoramica sul polimorfismo

Membri virtuali

Quando una classe derivata eredita da una classe di base, include tutti i membri della classe di base. Tutto il comportamento dichiarato nella classe di base fa parte della classe derivata. Ciò consente di considerare gli oggetti della classe derivata come oggetti della classe di base. I modificatori di accesso (public, protected, private e così via) determinano se tali membri sono accessibili dall'implementazione della classe derivata. I metodi virtuali offrono alla finestra di progettazione diverse opzioni per il comportamento della classe derivata:

  • La classe derivata può eseguire l'override dei membri virtuali nella classe di base, definendo un nuovo comportamento.
  • La classe derivata può ereditare il metodo di classe di base più vicino senza eseguirne l'override, mantenendo il comportamento esistente, ma consentendo ad altre classi derivate di eseguire l'override del metodo.
  • La classe derivata può definire una nuova implementazione non virtuale di quei membri che nascondono le implementazioni della classe di base.

Una classe derivata può eseguire l'override di un membro della classe base solo se quest'ultimo è dichiarato come virtuale o astratto. Il membro derivato deve usare la parola chiave override per indicare esplicitamente che il metodo deve partecipare alla chiamata virtuale. Nel codice seguente ne viene illustrato un esempio:

public class BaseClass
{
    public virtual void DoWork() { }
    public virtual int WorkProperty
    {
        get { return 0; }
    }
}
public class DerivedClass : BaseClass
{
    public override void DoWork() { }
    public override int WorkProperty
    {
        get { return 0; }
    }
}

I campi non possono essere virtuali; solo metodi, proprietà, eventi e indicizzatori possono essere virtuali. Quando una classe derivata esegue l'override di un membro virtuale, quest'ultimo viene chiamato anche nel caso in cui si acceda a un'istanza di tale classe come istanza della classe base. Nel codice seguente ne viene illustrato un esempio:

DerivedClass B = new DerivedClass();
B.DoWork();  // Calls the new method.

BaseClass A = B;
A.DoWork();  // Also calls the new method.

Metodi virtuali e proprietà consentono alle classi derivate di estendere una classe base senza dover usare l'implementazione della classe base di un metodo. Per altre informazioni, vedere Controllo delle versioni con le parole chiave Override e New. Un'interfaccia fornisce un'altra modalità per definire un metodo o un insieme di metodi la cui implementazione è lasciata alle classi derivate.

Nascondere un membro di una classe di base con nuovi membri

Se si desidera che la classe derivata abbia un membro con lo stesso nome di un membro in una classe di base, è possibile usare la nuova parola chiave per nascondere il membro della classe di base. La parola chiave new viene inserita prima del tipo restituito di un membro di classe che viene sostituito. Nel codice seguente ne viene illustrato un esempio:

public class BaseClass
{
    public void DoWork() { WorkField++; }
    public int WorkField;
    public int WorkProperty
    {
        get { return 0; }
    }
}

public class DerivedClass : BaseClass
{
    public new void DoWork() { WorkField++; }
    public new int WorkField;
    public new int WorkProperty
    {
        get { return 0; }
    }
}

È possibile accedere ai membri della classe di base nascosti dal codice client eseguendo il cast dell'istanza della classe derivata a un'istanza della classe di base. Ad esempio:

DerivedClass B = new DerivedClass();
B.DoWork();  // Calls the new method.

BaseClass A = (BaseClass)B;
A.DoWork();  // Calls the old method.

Impedire alle classi derivate di eseguire l'override dei membri virtuali

I membri virtuali rimangono sempre tali, indipendentemente dal numero di classi dichiarate fra i membri virtuali e la classe in cui è stato originariamente dichiarato il membro virtuale. Se la classe A dichiara un membro virtuale e la classe B deriva da Ae la classe C deriva da B, la classe C eredita il membro virtuale e può eseguirne l'override, indipendentemente dal fatto che la classe B abbia dichiarato o meno un override per tale membro. Nel codice seguente ne viene illustrato un esempio:

public class A
{
    public virtual void DoWork() { }
}
public class B : A
{
    public override void DoWork() { }
}

Una classe derivata può interrompere l'ereditarietà virtuale dichiarando un override come sealed. A tale scopo, è necessario inserire la parola chiave sealed prima della parola chiave override nella dichiarazione del membro della classe. Nel codice seguente ne viene illustrato un esempio:

public class C : B
{
    public sealed override void DoWork() { }
}

Nell'esempio precedente il metodo DoWork non è più virtuale per nessuna classe derivata da C. È ancora virtuale per le istanze di C, anche se viene eseguito il cast al tipo B o al tipo A. I metodi sealed possono essere sostituiti da classi derivate tramite la parola chiave new, come illustrato nell'esempio seguente:

public class D : C
{
    public new void DoWork() { }
}

In questo caso, se viene chiamato DoWork su D usando una variabile di tipo D, verrà chiamato il nuovo DoWork. Se una variabile di tipo C, B o A viene usata per accedere a un'istanza di D, una chiamata a DoWork seguirà le regole di ereditarietà virtuale, instradando tali chiamate all'implementazione di DoWork nella classe C.

Accedere a membri virtuali di una classe di base dalle classi derivate

Una classe derivata che ha sostituito un metodo o una proprietà, o ne ha eseguito l'override, può ancora accedere al metodo o alla proprietà sulla classe base usando la parola chiave base. Nel codice seguente ne viene illustrato un esempio:

public class Base
{
    public virtual void DoWork() {/*...*/ }
}
public class Derived : Base
{
    public override void DoWork()
    {
        //Perform Derived's work here
        //...
        // Call DoWork on base class
        base.DoWork();
    }
}

Per altre informazioni, vedere base.

Nota

Nell'implementazione dei membri virtuali è consigliabile l'uso della parola chiave base per le chiamate all'implementazione della classe base di tali membri. In questo modo, nella classe derivata sarà possibile definire la sola implementazione del comportamento specifico per tale classe. Se l'implementazione della classe base non viene chiamata, spetterà alla classe derivata rendere il proprio comportamento compatibile con quello della classe base.