Overloading significa «sovraccaricare» ovvero definire più versioni di un metodo, utilizzando lo stesso nome ma una firma diversa.
Per firma di un metodo si intede il numero e/o il tipo di argomenti nella dichiarazione. Sulla base degli argomenti effettivamente passati alla routine, verrà riconosciuta la firma e richiamato il metodo corretto.
Vediamo subito un esempio:
public static void Stampa(string Messaggio)
{
System.Console.WriteLine("Il messaggio è: " + Messaggio);
}
public static void Stampa(int Numero)
{
System.Console.WriteLine("Il numero è: " + Numero);
}
Le due definizioni della routine Stampa() differiscono solo per il tipo dell'argomento: string nel primo caso, int nel secondo. Richiamando questi due metodi, si ottiene il seguente risultato:
//Stampa: "Il messaggio è: Questa è una stringa".
Stampa("Questo è una stringa");
//Stampa: "Il numero è 15".
Stampa(15);
L'overloading viene spesso usato nella definizione del costruttore di una classe. Riprendiamo il costruttore della nostra classe Persona:
public Persona(string Nome, string Cognome)
{
if (Nome == string.Empty)
mNome = "(Nessun nome)";
else
mNome = Nome;
if (Cognome == string.Empty)
mCognome = "(Nessun cognone)";
else
mCognome = Cognome;
}
Finora, per creare una persona senza nome né cognome, era necessario passare esplicitamente la stringa vuota nel costruttore:
Persona p = new Persona("", "");
In questo caso, però, sarebbe preferibile poter istanziare la classe Persona senza passare alcun argomento al costruttore. Tale risultato può essere facilmente raggiunto con l'overloading del costruttore:
public Persona()
{
mNome = "Nessun nome";
mCognome = "Nessun cognome";
}
public Persona(string Nome, string Cognome)
{
mNome = Nome;
mCognome = Cognome;
}
Così facendo, se si crea una Persona indicando nome e cognome, essi verranno salvati nelle variabili mNome e mCognome, poiché viene richiamato il costruttore Persona(string Nome, string Cognome), mentre, se si crea una Persona senza parametri, è invocato il costruttore Persona(), che imposta mNome e mCognome, rispettivamente, su "Nessun nome" e "Nessun cognome".
//Imposta: mNome = Marco, mCognome = Minerva
Persona p1 = new Persona("Marco", "Minerva");
//Imposta mNome = Nessun nome, mCognome = Nessun cognome
Persona p2 = new Persona();
Un costruttore può chiamarne un altro. Nel nostro esempio, anziché assegnare esplicitamente le variabili mNome e mCognome nel costruttore senza argomenti, sarebbe stato possibile richiamare l'altro costruttore, "dicendogli" come impostare il nome e il cognome della persona:
public Persona() : this("Nessun nome", "Nessun cognome") {}
La parola chiave «this» permette di avere accesso ai metodi, alle variabili ed alle proprietà della classe corrente, così come «base» permette di avere accesso alla classe da cui si eredita. Il corrispettivo di «this» in VB.NET è «Me».
Questa soluzione è da preferire poiché, in caso di modifiche al costruttore, è sufficiente intervenire su un solo punto del codice.
Se, ad esempio, dopo aver creato la classe Persona, si decidesse che il nome deve essere sempre preceduto dalla stringa "Sig.", nel primo esempio di overloading del costruttore sarebbe stato necessario modificare il codice in due parti diverse.
Nel caso in cui siano previsti più costruttori, le porzioni di codice da cambiare possono essere più numerose e, quindi, aumenta la probabilità di dimenticarsi qualche modifica.
La nuova classe Persona è disponibile qui.
Overloading degli operatori
L'overloading degli operatori, derivato dal C++, consiste nel ridefinire il comportamento di operatori come +, - , <, >, ==, etc., affinchè assumano comportamenti a seconda degli oggetti cui sono applicati.
C# supporta l'overloading degli operatori a partire dalla sua prima versione, mentre VB.NET ha introdotto questa funzionalità solo con la versione 2.0 del Framework.
Partiamo come sempre dalla classe Persona. Non avendo altre informazioni, C# assume che due variabili di tipo Persona siano uguali se fanno riferimento all'istanza dello stesso oggetto, ovvero:
Persona p1 = new Persona("Donald", "Duck");
Persona p2 = new Persona("Donald", "Duck");
//Stampa true
Console.WriteLine(p1 == p1);
//Stampa False, gli oggetti p1 e p2 sono diversi
Console.WriteLine(p1 == p2);
//Ora p2 fa riferimento alla stessa istanza di p1
p2 = p1;
//Stampa true
Console.WriteLine(p1 == p2);
Quello che vorremmo, invece, è che due persone risultassero uguali se il nome e il cognome fossero gli stessi (nell'esempio sopra riportato, vorremmo che il primo confronto 'p1 == p2' restituisse true).
Per fare questo, o ogni volta facciamo il controllo sulle proprietà Nome e Cognome oppure, più semplicemente, ridefiniamo l'operatore '==' per la classe Persona creando un metodo con la parola chiave «operator»:
public static bool operator ==(Persona p1, Persona p2)
{
if (p1.Nome == p2.Nome && p1.Cognome == p2.Cognome)
return true;
return false;
}
public static bool operator !=(Persona p1, Persona p2)
{
if (p1.Nome != p2.Nome || p1.Cognome != p2.Cognome)
return true;
return false;
}
Bisogna ridefinire insieme gli operartori che stabiliscono relazioni simili. Avendo ridefinito l'operatore '==', il Framework ci richiede che venga ridefinito anche l'operatore '!=' (e viceversa), così come, se avessimo ridefinito <, avremmo dovuto definire anche > (e viceversa).
L'overloading degli operatori è sempre definito con metodi statici. Dopo l'indicazione del tipo di dato restituito (solitamente bool oppure lo stesso tipo della classe per cui si sta creando l'overloading, a seconda dell'operatore in questione), è necessario specificare l'operatore che si sta ridefinendo.
Sulla guida in linea troviamo l'elenco di tutti gli operatori che possono essere sottoposti ad overloading.
Infine, bisogna indicare a quali tipi di dati si applica: nel nostro esempio, '==' e '!=' si applicano a oggetti della classe Persona.
Gli argomenti del metodo sono due nel caso di operatori binari (come mostrato nel nostro esempio), oppure uno solo se l'operatatore è unario (++, --, ecc.).
Ridefinendo gli operatori '==' e '!=', il compilatore si aspetta che vengano anche ridefiniti i metodi Equals() e GetHashCode(), che ogni classe possiede in quanto li eredita dalla classe base Object. Ad ogni modo, se non vengono specificati, si ottiene solo un warning di compilazione. Definiamoli comunque, per avere una classe più robusta:
public override bool Equals(object o)
{
if (o is Persona)
return this == (Persona) o;
return (object)this == o;
}
public override int GetHashCode()
{
//Questa routine dovrebbe restituire un codice hash identificativo
//dell'istanza corrente della classe.
return 0;
}
Nel metodo Equals() si è utilizzato l'operatore «is», necessario per controllare che l'oggetto passato alla funzione sia di tipo Persona: in caso positivo, l'oggetto viene convertito e il controllo sull'uguaglianza è fatto utilizzando la nostra definizione di '=='.
Altrimenti, l'oggetto corrente viene convertito ad Object, quindi si usa il confronto standard, per cui due variabili di tipo object sono uguali se fanno riferimento all'istanza dello stesso oggetto.
Grazie a questa ridefinizione, ora possiamo ottenere il risultato desiderato:
Persona p1 = new Persona("Donald", "Duck");
Persona p2 = new Persona("Donald", "Duck");
Console.WriteLine(p1 == p2); //Stampa true
Console.WriteLine(p1.Equals(p2)); //Stampa true
Cliccare qui per scaricare la classe Persona realizzata fino a questo punto.
A titolo di esempio, nel file da scaricare è stato ridefinito anche l'operatore '+', che "somma" due persone, ovvero crea una nuova Persona il cui nome e cognome sono la concatenazione dei nomi e dei cognomi delle persone di partenza. Al di là dell'utilità di un'operazione del genere, si tratta comunque di un esempio in più da tenere in considerazione.