Overloading di Procedure e Funzioni in Object Pascal

In questa lezione esploreremo l'overloading delle funzioni e delle procedure in Object Pascal. L'overloading è una tecnica che consente di creare più versioni di una stessa funzione o procedura con lo stesso nome, ma con un numero di parametri diverso o tipi di parametro diversi. Questo permette di creare funzioni e procedure più flessibili e facilmente utilizzabili nel codice.

Analizzeremo il concetto di signature o firma di una funzione o procedura e come essa dipenda solo dal nome della funzione e dal tipo di parametri.

Inoltre, vedremo come i parametri di default possono essere utilizzati per rendere le funzioni e le procedure ancora più flessibili e facili da usare.

Overloading delle funzioni

Spesso, nella realizzazione di programmi complessi, può sorgere l'esigenza di dover realizzare funzioni o procedure che effettuano operazioni simili ma su tipi differenti.

Supponiamo, ad esempio, di voler implementare una funzione che restituisca il massimo tra due numeri. Possiamo voler implementare una funzione che restituisca il massimo tra due interi e una funzione che restituisca il massimo tra due numeri floating point.

Una possibile soluzione a questo problema è quella di implementare due funzioni con nomi differenti per i due casi:

{ Versione per interi }
function MaxInteger(a, b: Integer): Integer;
begin
    if a < b then
        Result := b
    else
        Result := a;
end;

{ Versione per floating point }
function MaxFloat(a, b: Double): Double;
begin
    if a < b then
        Result := b
    else
        Result := a;
end;

Anche se valida, questa soluzione ha il problema di dover definire due funzioni con nomi differenti. Tuttavia, da un punto di vista semantico, esse svolgono la stessa operazione ma su tipi differenti.

In questi casi, in Object Pascal è possibile sfruttare l'overloading delle funzioni. In pratica è possibile definire funzioni con lo stesso nome ma con tipi di parametri differenti. Per farlo è sufficiente far seguire l'intestazione della funzione dalla parola chiave overload:

function NomeFunzione( { parametri ... } ); overload;

Tornando all'esempio di prima, possiamo chiamare entrambe le funzioni come Max utilizzando l'overloading in questo modo:

{ Versione per interi }
function Max(a, b: Integer): Integer; overload;
begin
    if a < b then
        Result := b
    else
        Result := a;
end;

{ Versione per floating point }
function Max(a, b: Double): Double; overload;
begin
    if a < b then
        Result := b
    else
        Result := a;
end;

In questo modo il compilatore comprende, in base al tipo dei parametri passati alla funzione, quale delle due implementazioni deve utilizzare. Ad esempio:

WriteLn(Max(4, 5));
WriteLn(Max(4e-3, 5e-3));

Nel primo caso, il compilatore utilizzerà la versione di Max che accetta due parametri Integer. Nel secondo caso utilizzerà la versione di Max che accetta come parametri due Double.

In tal caso l'output di questo segmento di codice sarà:

5
 5.0000000000000001E-003

Se si prova a chiamare la funzione con parametri i cui tipi non corrispondono a nessuna versione in overload, il compilatore segnalerà un errore:

WriteLn(Max(4, 'Ciao'));

Il compilatore FreePascal restituirà un errore del genere:

prova_overload.pas(39,26) Error: Incompatible type for arg no. 2: Got "Constant String", expected "Double"

L'overloading delle funzioni funziona esclusivamente sul tipo dei parametri. Per l'overloading il compilatore non considera il valore di ritorno della funzione. Ad esempio, il seguente pezzo di codice non è valido:

function Somma(a, b: Integer): Integer; overload;
begin
    Result := a + b;
end;

function Somma(a, b: Integer): Double; overload;
begin
    Result := a + b;
end;

Le due versioni della funzione Somma differiscono, infatti, solo per il valore di ritorno.

Inoltre, in caso di overloading il nome dei parametri non conta. Infatti, il seguente codice provoca la segnalazione di un errore:

function Somma(a, b: Integer): Integer; overload;
begin
    Result := a + b;
end;

function Somma(c, d: Integer): Integer; overload;
begin
    Result := c + d;
end;

Le due funzioni hanno gli stessi tipi dei parametri anche se nomi diversi. In questo caso il compilatore FreePascal restituirà un errore del tipo:

prova_overload.pas(28,10) Error: overloaded functions have the same parameter list

Si può usare l'overloading, invece, per realizzare funzioni con differente numero di parametri. Tornando all'esempio di prima, possiamo realizzare due funzioni Max che lavorano su interi, dove la prima prende in ingresso due parametri e la seconda ne prende tre:

{ Versione a due parametri }
function Max(a, b: Integer): Integer; overload;
begin
    if a < b then
        Result := b
    else
        Result := a;
end;

{ Versione a tre parametri }
function Max(a, b, c: Integer): Integer; overload;
begin
    if a < b then
        if b < c then
            Result := c
        else
            Result := b
    else
        if a < c then
            Result := c
        else
            Result := a;
end;

In base al numero di parametri passati, il compilatore capirà quale delle due versioni utilizzare:

{ Il compilatore chiama la prima versione }
WriteLn(Max(3, 4));

{ Il compilatore chiama la seconda versione }
WriteLn(Max(3, 4, 5));

Riassumendo:

Definizione

Overloading delle funzioni

In Object Pascal è possibile creare più versioni di una stessa funzione o procedura con lo stesso nome, ma con un numero diverso o tipi diversi di parametri. Questa capacità prende il nome di Overloading delle Funzioni, in italiano Sovraccarico delle funzioni.

Per poter usare l'overloading è necessario che tutte le funzioni con lo stesso nome siano definite con la parola chiave overload. La sintassi è la seguente:

function nome_funzione(parametro: tipo_1): tipo_ritorno_1; overload;
begin
    { Corpo della versione 1}
end;

function nome_funzione(parametro: tipo_2): tipo_ritorno_1; overload;
begin
    { Corpo della versione 2}
end;

L'overloading delle funzioni tiene presente soltanto del tipo e del numero dei parametri.

L'overloading delle funzioni non tiene conto del nome dei parametri o del tipo del valore di ritorno.

Signature di una funzione

Un concetto fondamentale che riguarda le procedure e le funzioni in Object Pascal è la cosiddetta Signature, o Firma.

La signature di una funzione è un identificativo univoco assegnato dal compilatore a quella funzione o procedura. Essa dipende dal nome della funzione e dai tipi dei parametri di ingresso.

Senza entrare nel dettaglio, possiamo chiarire il tutto con un esempio. Supponiamo di avere una funzione Somma che effettua la somma di due numeri interi:

function Somma(x: Integer; y: Integer); overload;
begin
    Result := x + y;
end;

La signature o firma di questa funzione sarà un qualcosa del tipo: Somma_Integer_Integer.

Se proviamo a scrivere una versione di Somma che lavora con numeri in virgola mobile otteniamo:

function Somma(x: Double, y: Double); overload;
begin
    Result := x + y;
end;

Internamente il compilatore assegnerà a questa funzione una firma del tipo: Somma_Double_Double.

Come si può osservare le firme delle due funzioni sono differenti. Grazie a questo meccanismo il compilatore è in grado di selezionare quale funzione usare a seconda dei parametri. In altre parole, la signature di una funzione è il meccanismo alla base dell'overloading.

Ne consegue che se vogliamo fare overload di una funzione, la nuova versione dovrà avere una signature diversa.

Poiché la signature non dipende dal valore di ritorno e dal nome dei parametri risulta ora più chiaro perché l'overloading non tiene conto di questi due fattori.

Definizione

Signature di una funzione

La Signature o Firma di una funzione è rappresentata dal nome della funzione e dalla lista dei tipi dei parametri in ingresso della funzione.

La Signature viene usata internamente dal compilatore per gestire l'overloading delle funzioni.

Si può fare overload di una funzione soltanto definendo una funzione con signature diversa.

Parametri di Default

Un'altra funzionalità messa a disposizione dal linguaggio Object Pascal che è legata all'overloading delle funzioni è la possibilità di passare dei valori di default.

In pratica, attraverso i parametri di default è possibile invocare una funzione o procedura con un numero di argomenti inferiore. I restanti parametri assumeranno i valori di default specificati nella definizione della funzione.

Chiariamo con un esempio. Supponiamo di voler realizzare una semplice funzione che elevi a potenza un numero intero. Questa funzione prende in ingresso sia la base b che l'esponente e. Possiamo realizzare la funzione in questo modo:

function potenza(b: Integer; e: Integer): Integer;
var
    I: Integer;
    accumulatore: Integer;
begin
    accumulatore := 1;
    for I := 1 to e do
    begin
        accumulatore := accumulatore * b;
    end;
    Result := accumulatore;
end;

Per come è definita, questa funzione richiede sempre due parametri in ingresso. Supponiamo, ad esempio, che nel nostro programma invochiamo molto frequentemente questa funzione per calcolare il quadrato di un numero. Per cui nella maggior parte dei casi nel nostro programma il secondo parametro sarà pari a 2.

Possiamo specificare che l'esponente e sia di default pari a 2 in questo modo:

function potenza(b: Integer; e: Integer = 2): Integer;

Così facendo, possiamo invocare la funzione sia con due parametri, specificando base ed esponente, sia con un solo parametro. In quest'ultimo caso l'esponente e assumerà il valore di default pari a 2.

{ Invocazione con due parametri }
risultato := potenza(4, 3);

{ Invocazione con un parametro }
{ Equivalente a potenza(4, 2) }
risultato := potenza(4);

Nell'utilizzo dei parametri di default, tuttavia, vi sono dei limiti.

In primo luogo, i parametri di default devono essere per forza gli ultimi della lista. In altre parole, non è possibile definire un parametro di default seguito da un parametro non di default.

Questa definizione, ad esempio, è valida:

{ Definizione valida }
function prova(p1: Integer; p2: Integer; p3: Integer = 5): Integer;

Quest'altra definizione, invece, non è valida:

{ Definizione NON valida }
function prova(p1: Integer = 9; p2: Integer; p3: Integer): Integer;

In secondo luogo, quando si invoca una procedura o funzione con parametri di default si possono omettere gli argomenti a partire dall'ultimo. In altre parole, se si omette un parametro bisogna omettere anche gli altri parametri che seguono.

Ad esempio, prendiamo la funzione che segue:

function prova(p1: Integer; p2: Integer = 4; p3: Integer = 5): Integer;

Se vogliamo omettere p2 dobbiamo necessariamente omettere anche p3:

{ p2 = 4 e p3 = 5}
risultato := prova(1);

In questo esempio se omettiamo p2 non possiamo assegnare p3.

Infine, l'ultimo limite, riguarda il fatto che i parametri di default possono essere passati soltanto per copia oppure devono essere parametri costanti. Non è possibile passare parametri di default per riferimento.

Ricapitolando:

Definizione

Parametri di Default

In Object Pascal è possibile assegnare dei valori di default ai parametri di una funzione o procedura. Tali parametri di default possono essere omessi in fase di invocazione. In caso di omissione il compilatore provvederà ad assegnare a tali parametri il valore specificato nella definizione della procedura o funzione.

La sintassi per specificare il valore di default di un parametro è la seguente:

function nome(p1: tipo_1; p2: tipo_2; p3: tipo_3 = valore_default): tipo_ritorno;

L'uso dei parametri di default ha i seguenti limiti:

  • I parametri di default devono essere sempre alla fine della lista dei parametri;
  • Se un parametro di default viene omesso devono essere omessi anche i parametri di default che seguono;
  • Il passaggio dei parametri di default può avvenire solo per copia o come costanti;
  • Il valore di default deve essere un valore costante noto a tempo di compilazione.

Parametri di Default e Overloading

Quando si mescolano insieme parametri di Default e Overloading si potrebbe incappare in delle situazioni di errore.

Prendiamo l'esempio che segue:

{ Prende un numero in ingresso e lo stampa n volte }
{ Separa ogni numero con un carattere c }
{ Di default c = '-' }
procedure StampaNVolte(x: Integer; n: Integer; c: Char = '-'); overload;
var
    I: Integer;
begin
    for I := 1 to n do
    begin
        Write(x, c);
    end;
    Writeln;
end;

Questa procedura prende in ingresso un numero intero x e lo stampa n volte separando ogni stampa con il carattere c. Di default c è pari al carattere '-'.

Possiamo invocare la procedura in questo modo:

StampaNVolte(3, 5, ' ');

Il risultato che otteniamo è il seguente:

3 3 3 3 3

Possiamo omettere il parametro c in questo modo:

StampaNVolte(3, 5);

Otteniamo il seguente risultato:

3-3-3-3-3-

Supponiamo, adesso, di voler creare una versione modificata della funzione che come terzo parametro, invece di prendere un carattere, prenda in ingresso un intero che rappresenta il numero di spazi da inserire tra le stampe. Possiamo scriverla in questo modo:

{ Prende un numero in ingresso e lo stampa n volte }
{ Separa ogni numero con un numero di spazi pari a m }
{ Di default m = 1 }
procedure StampaNVolte(x: Integer; n: Integer; m: Integer = 1); overload;
var
    I: Integer;
    J: Integer;
begin
    for I := 1 to n do
    begin
        Write(x);
        for J := 1 to m do
            Write(' ');
    end;
    Writeln;
end;

Sfruttando l'overloading possiamo invocare una delle due funzioni in base ai parametri passati. Ad esempio:

{ Invoca la prima versione }
StampaNVolte(3, 5, '*');

{ Invoca la seconda versione }
StampaNVolte(3, 5, 2);

Tuttavia, se omettiamo il terzo parametro, il compilatore ci segnala un errore:

{ Errore: Chiamata Ambigua }
StampaNVolte(3, 5);
Error: Can't determine which overloaded function to call

Questo perché ci troviamo di fronte ad una chiamata ambigua. Infatti, il compilatore non sa quale delle due versioni della funzione usare. Poiché entrambe le funzioni hanno un parametro di default il compilatore non sa quale delle due usare: se quella con il terzo parametro Char o con il terzo parametro Integer.

Per questo motivo è sempre meglio cercare di evitare di combinare Overloading e Parametri di Default.

Nota

Evitare di combinare Parametri di Default e Overloading

In Object Pascal è sempre buona norma evitare di combinare insieme l'overloading di funzioni e procedure con i parametri di default. Si potrebbero verificare errori di chiamate ambigue a funzione.

In Sintesi

In questa lezione abbiamo studiato l'overloading delle funzioni ossia la possibilità di scrivere più versioni di una funzione con lo stesso nome ma con numero di parametri e tipi differenti.

Il vantaggio dell'overloading sta più dal lato dell'utilizzatore che non deve utilizzare nomi differenti di una funzione a seconda che usi una versione pensata per un tipo rispetto ad un'altra. Si pensi al caso di una funzione che calcoli il massimo di 2, 3, e 4 elementi. L'utilizzatore potrà usare lo stesso nome Max per calcolare il massimo indipendentemente dal numeri di elementi:

Max(3, 2);
Max(5, 8, 2);
Max(6, 2, 3);

Questo aiuta i programmi ad essere più leggibili e manutenibili.

Abbiamo poi studiato i parametri di default che permettono di omettere gli argomenti in fase di invocazione. Essi sono utili nei casi in cui è raro o poco frequente che un parametro possa cambiare rispetto ad un valore di default.