Introduzione
In questo articolo approfondisco una parte fondamentale della programmazione: il passaggio di parametri, con particolare riferimento al linguaggio C#.
In molti linguaggi, soprattutto compilati, Γ¨ possibile passare argomenti a parametri di funzioni per valore o per riferimento.
Questo articolo Γ¨ una parziale traduzione di un vecchio articolo di Jon Skeet che si trovava in qui e ora si puΓ² reperire solo su WebArchive; questo mio lavoro servirΓ quindi anche come backup per il futuro.
La conoscenza di questa differenza e la sua padronanza Γ¨ indispensabile per una buona programmazione.
Un ottimo esempio della differenza tra i due lβho trovata spiegata in questa domanda di StackOverflow: assumiamo che io voglia condividere una pagina web con te: se ti fornisco lβurl della pagina sto facendo un passaggio per riferimento, infatti se la pagina cambia entrambi vediamo gli stessi cambiamenti. Qualora tu elimini lβurl non stai eliminando la pagina in se, ma solo il modo che tu hai per accedere a tale pagina.
Se invece stampo la pagina su un foglio e te lo fornisco, allora sto effettuando un passaggio per valore: la tua pagina Γ¨ disconnessa dallβoriginale, le modifiche che tu effettui o che vengono effettuate sullβoriginale, non vengono rilevate.
Tipi di variabile: referenza e valore
In C# esistono due insiemi di tipi di variabili: i tipi per referenza e i tipi per valore.
Tipi per referenza (reference types)
Un tipo per referenza Γ¨ un tipo che ha come valore il riferimento ai dati invece che i dati stessi. Per esempio, consideriamo il codice seguente:
StringBuilder sb = new StringBuilder();
In questa riga di codice abbiamo dichiarato una variabile sb
, creato un nuovo oggetto StringBuilder
e assegnato a sb un riferimento a tale oggetto.
Il valore di sb
non Γ¨ lβoggetto stesso, ma la sua referenza, come si puΓ² capire dallβesempio seguente:
StringBuilder first = new StringBuilder();
first.Append("hello");
StringBuilder second = first; // second Γ¨ un puntatore alla stessa area di memoria di first
first.Append(" world");
Console.WriteLine(second); // Scrive ciò a cui punta first, cioè "hello world"
Eβ importante sottolineare che le due variabili first
e second
sono variabili indipendenti, se first puntasse a una nuova area di memoria second punterebbe ancora alla vecchia area di first, come si vede da questo esempio:
StringBuilder first = new StringBuilder();
first.Append("hello");
StringBuilder second = first;
first.Append(" world");
first = new StringBuilder("goodbye"); // ora first punta ad un nuova area di memoria
Console.WriteLine(first); // Prints goodbye // quindi scrive "goodbye"
Console.WriteLine(second); // Still prints hello world // second punta ancora all'area di memoria originale e conseguentemente scrive "hello world"
Tipi per valore (value types)
Le variabili di tipo per valore contengono direttamente i dati e non cβΓ¨ quindi un livello intermedio di puntatore β dati. Lβassegnamento di una variabile di questo tipo presuppone che tutti i dati vengano copiati.
Per esempio, consideriamo la seguente struct:
public struct IntHolder
{
public int i;
}
Quando lavoro con una variabile di tipo IntHolder
, questa contiene tutti i dati (nel caso seguente un valore intero). Un assegnamento copia il valore, slegandolo dalla variabile iniziale
IntHolder first = new IntHolder();
first.i = 5;
IntHolder second = first; // second punta ad una nuova area di memoria contenente tutti i dati di first
first.i = 6;
// La linea seguente scrive "5", non Γ¨ influenzata dalla modifica
Console.WriteLine (second.i);
Tutti i tipi semplici, compresi gli struct e gli enum (ma non string) sono tipi per valore.
Il tipo string
Γ¨ un tipo particolare in quanto spesso si comporta come se fosse un tipo per valore invece Γ¨ un tipo per referenza a tutti gli effetti. Questi tipi di oggetti sono detti tipi immutabili, che significa che una volta che Γ¨ stata creata unβistanza di questi, non puΓ² piΓΉ essere cambiata. Questo permette ad un tipo per referenza di comportarsi in maniera simile ad un tipo per valore in quanto posso passarlo a dei metodi black box ed essere sicuro che questi non ne possano cambiare il valore.
Le tipologie di passaggio di parametri
In C# esistono quattro differenti tipologie di parametri:
- value parameters: default;
- reference parameters: parola chiave ref;
- output parameters: parola chiave out;
- array parameters: parola chiave params.
Per un approfondimento sui parametri di metodo vedi questo articolo.
Eβ possibile utilizzare qualsiasi di questi parametri sia con tipi di riferimento che valore.
Eβ sempre importante sottolineare che un conto Γ¨ se un tipo di dato Γ¨ un reference type o value type, un altro conto Γ¨ se il passaggio di un determinato parametro ad una funziona avviene per riferimento o per valore, sono due concetti diversi.
Value parameters
I parametri passati alle funzioni sono, di default, value parameters.
Questo significa che viene creata una nuova area di memoria per la variabile dichiarata nella firma della funzione e questa area viene inizializzata con il valore che specifichi nellβinvocazione della funzione.
Questo Γ¨ il comportamento di default della maggior parte dei linguaggi.
Se la funzione chiamata modifica questo valore la funzione chiamante non potrΓ vedere questa modifica.
Questo Γ¨ un punto particolarmente delicato, sopratutto in C#: nel seguente codice una classe (reference type) viene passato ad un metodo; allβinterno del metodo viene assegnata a null: questa modifica non si riflette allβesterno. PerchΓ©?
void Foo (StringBuilder x)
{
x = null; // x ora punta a null
}
StringBuilder y = new StringBuilder();
y.Append ("hello");
Foo (y);
Console.WriteLine (y==null); // False. y non Γ¨ stato modificato
Se invece nel metodo Foo
non ho un assegnamento a null
ma ho una modifica ad una property questa invece viene riflessa anche allβesterno:
void Foo (StringBuilder x)
{
x.Append (" world"); // x (che punta alla stessa area di memoria di y
}
StringBuilder y = new StringBuilder();
y.Append ("hello");
Foo (y);
Console.WriteLine (y); // print "hello world"
Questo comportamento, allβapparenza contraddittorio, Γ¨ invece perfettamente coerente: lβaffermazione βin C# gli oggetti complessi (come le classi) sono passati per riferimento e mai per valoreβ Γ¨ estremamente confusionaria (come suggerito da Jon Skeet) in quanto lβaffermazione corretta sarebbe βi di default i riferimenti agli oggetti sono passati per valoreβ.
Infatti nellβesempio sopra Γ¨ evidente che il riferimento alla variabile y viene copiato nella variabile x (passaggio per valore) ma, essendo un riferimento, la modifica alle property di x allβinterno del metodo si riflette nella variabile x fuori dal metodo.
Di conseguenza Γ¨ evidente che impostare x
a null non porta a nulla sulla variabile y
in quanto x
Γ¨ una variabile diversa il cui riferimento Γ¨ stato copiato da y
.
Reference parameters
I reference parameters non passano il valore della variabile ma direttamente la variabile effettiva: invece di creare una nuova locazione di memoria viene utilizzata la stessa del chiamante.
Utilizzando questi parametri ogni modifica che avviene allβinterno del metodo (anche lβindirizzo a cui punta la variabile) si rifletterΓ allβesterno.
In C# i reference parameters devono essere esplicitati usando la parola chiave ref
nella dichiarazione del metodo:
void Foo (ref StringBuilder x)
{
x = null; // x === y, quindi impostare a null viene riflesso anche sul chiamante.
}
StringBuilder y = new StringBuilder();
y.Append ("hello");
Foo (ref y);
Console.WriteLine (y==null); // True
Ovviamente Γ¨ possibile anche passare un value type (come uno struct
) per reference: in questo caso i valori non vengono copiati e conseguentemente ogni modifica si riflette anche allβesterno:
void Foo (ref IntHolder x)
{
x.i=10;
}
IntHolder y = new IntHolder();
y.i=5;
Foo (ref y);
Console.WriteLine (y.i); // Scrive 10
Combinazioni
Come abbiamo visto ho due tipologie di tipi di dato (referenza e valore) e due tipologie di passaggio di parametri (referenza e valore), vediamo quindi le 4 combinazioni possibili:
Value types passati per valore
Questo Γ¨ il comportamento di default quando viene passato un value type ad una funzione: viene creata una nuova variabile, copiato tutto il contenuto della prima e passata allβinterno del metodo.
Ogni modifica allβinterno del metodo non si riflette allβesterno.
Value types passati per referenza
Utilizzando la parola chiave ref posso passare per referenza anche un value type: non ho alcuna copia e la variabile che uso allβinterno del metodo Γ¨ la stessa del chiamante: qualsiasi modifica viene riflessa.
Reference types passati per valore
Questo Γ¨ il comportamento di default anche per i reference type: viene creata una nuova variabile e viene copiato lβindirizzo a cui punta la variabile del chiamante.
Le modifiche alle property dellβoggetto si riflettono allβesterno (puntano entrambe le variabili alla stessa cosa) ma un eventuale assegnamento ad una nuova area di memoria non viene riflesso.
Reference types passati per riferimento
Il comportamento Γ¨ analogo ai value type passati per riferimento: viene passato un puntatore e tutte le modifiche interne al metodo vengono riflesse allβesterno.
Differenze tra i linguaggi
Non tutti i linguaggi permettono entrambe le modalitΓ di passaggio dei parametri, di seguito indico i linguaggi piΓΉ comuni e la sintassi per il passaggio di parametri per valore o referenza.
Linguaggio | Passaggio per valore | Passaggio per referenza |
---|---|---|
C | call_by_value(int p) | call_by_reference(int & p) |
C++ | call_by_value(int p) | call_by_reference(int & p) |
C# | call_by_value(int p) | call_by_value(int ref p) |
Java | ogni cosa in Java Γ¨ passata per valore | - |
Python | ogni cosa in Python Γ¨ passata per valore | indirettamente, tramite i mutable objects |
PHP | call_by_value(int $p) | call_by_reference(&$p) |
Javascript | ogni cosa in Javascript Γ¨ passata per valore | - |
Matlab | ogni cosa in Matlab Γ¨ passata per valore | - |
Come si puΓ² notare spesso i linguaggi interpretati non offrono la possibilitΓ di poter passare una variabile ad un metodo per referenza (tranne il PHP), mentre i linguaggi compilati puri (come il C o il C++) e i compilati su bytecode (come il Java o C#) offrono tale possibilitΓ .
Approfondimento: ref struct
Le struct βrefβ sono un costrutto introdotto in C# 7.2 e permettono di definire delle strutture dati di tipo struct che per costruzione non possono andare sullo Heap ma risiedono sempre sullo Stack.
LβimpossibilitΓ di andare sullo heap permettono di avere codice sicuramente piΓΉ leggero dal punto di vista della memoria e che richiede meno interventi del GC, ma porta con se molte limitazioni.
Anche se viene utilizzata la parola chiave βref
β analogamente al passaggio per referenza non centra nulla.
Per approfondire questo e altri concetti consiglio questo articolo.
Per approfondire: