Menu

Parametri ref, out e in in C#: il passaggio per riferimento spiegato

Come funzionano i parametri ref, out e in in C#: passare una variabile invece di una copia, out per risultati aggiuntivi e lo schema TryParse, out var, parametri in di sola lettura, le regole di assegnazione certa e cosa cambia quando l'argomento è un tipo riferimento.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Di default C# passa ogni argomento per valore: il metodo riceve una copia, e assegnare un valore al parametro non tocca il chiamante. I modificatori ref, out e in passano invece la variabile stessa del chiamante.

Output:

by value: 50
by ref:   60

La parola chiave compare due volte: nella lista dei parametri (ref int score) e nel punto di chiamata (ref score). Scriverla nella chiamata è obbligatorio, ed è voluto: chiunque legga AddPoints(ref score) sa che score può cambiare. Se la dimentichi, il compilatore segnala CS1620, Argument 1 must be passed with the 'ref' keyword.

ref: leggere e scrivere la variabile del chiamante

Un parametro ref è un alias della variabile del chiamante. Il metodo può leggerne il valore attuale e sostituirlo. L'esempio classico è lo scambio di due valori, impossibile con i parametri normali:

Output:

Linus Ada
volume 100

Regole per ref:

  • L'argomento deve essere una variabile (una variabile locale, un parametro, un campo o un elemento di un array), non un letterale né un'espressione. Nemmeno una proprietà va bene: Clamp(ref player.Volume, 0, 100) è l'errore CS0206, A non ref-returning property or indexer may not be used as an out or ref value. Copia la proprietà in una variabile locale, passa quella e poi riassegnala.
  • La variabile deve essere assegnata con certezza prima della chiamata. int n; Clamp(ref n, 0, 10); è CS0165, Use of unassigned local variable 'n'.

out: risultati aggiuntivi

out serve per i valori che il metodo produce. Il chiamante non deve inizializzare la variabile, e il metodo deve assegnarla su ogni percorso prima di restituire il controllo:

static void Split(string fullName, out string first, out string last)
{
    // error CS0177: The out parameter 'last' must be assigned to before control leaves the current method
    first = fullName.Split(' ')[0];
}

Dentro il metodo, un parametro out conta come non assegnato finché non ci scrivi, quindi non puoi leggere il vecchio valore del chiamante. È questa la differenza rispetto a ref: ref è bidirezionale, out va in una sola direzione, dal metodo al chiamante.

Lo schema TryParse

L'uso più noto di out è lo schema Try della libreria di base: restituire un bool per il successo e mettere il risultato in un parametro out, così un input non valido non lancia mai eccezioni:

Output:

"vip25" is a coupon: 25% off
"42" is a quantity: 42
"BOGUS" is not recognized
"" is not recognized

out var quantity (C# 7.0) dichiara la variabile direttamente nella chiamata; il suo tipo viene dal parametro. In caso di fallimento, TryParse la imposta a 0 e restituisce false. Quando ti interessa solo sapere se il parsing riesce, scarta il valore con out _: bool isNumber = int.TryParse(input, out _);.

Nota come TryGetDiscount assegna rate su entrambi i percorsi: una volta in modo esplicito e una volta passandolo a Dictionary.TryGetValue, che è a sua volta una chiamata con out.

out o restituire una tupla

Prima di C# 7, out era il modo principale per restituire più valori. Oggi le tuple lo fanno in modo più diretto:

// with out parameters
static void MinMax(int[] values, out int min, out int max) { ... }
MinMax(temps, out int lo, out int hi);

// with a tuple
static (int Min, int Max) MinMax(int[] values) { ... }
var (lo, hi) = MinMax(temps);

Usa una tupla (o una piccola classe) per più risultati semplici. Tieni out per lo schema Try, dove il bool restituito permette di mettere la chiamata dentro la condizione di un if.

Tipi riferimento: modificare l'oggetto o sostituirlo

Passare per valore un'istanza di una classe permette già al metodo di modificare l'oggetto, perché il valore copiato è un riferimento allo stesso oggetto. ref aggiunge un'ulteriore possibilità: sostituire l'oggetto a cui punta la variabile del chiamante.

Output:

after AddItem:      2 items
after ResetByValue: 2 items
after ResetByRef:   0 items

ResetByValue ha fatto puntare la sua copia del riferimento a una nuova lista; il chiamante non l'ha mai vista. ResetByRef ha modificato la variabile stessa del chiamante. Nel codice di tutti i giorni raramente serve ref su un parametro di tipo classe: restituisci piuttosto il nuovo oggetto.

Per le struct la differenza è maggiore, dato che un argomento struct viene copiato campo per campo. Un metodo che deve modificare una struct del chiamante (un Point, una grande struct di impostazioni) ha bisogno di ref.

in: sola lettura per riferimento

C# 7.2 ha aggiunto in, che passa per riferimento ma vieta l'assegnazione. Il suo scopo sono le prestazioni: una struct grande non viene copiata a ogni chiamata, e al chiamante è garantito che non cambierà.

// C# 7.2 and later
struct Matrix4
{
    public double M11, M12, M13, M14, M21, M22, M23, M24,
                  M31, M32, M33, M34, M41, M42, M43, M44;
}

static double Trace(in Matrix4 m) => m.M11 + m.M22 + m.M33 + m.M44;

static void Reset(in Matrix4 m)
{
    m.M11 = 0;
    // error CS8332: Cannot assign to a member of variable 'm' or use it as the right hand side of a ref assignment because it is a readonly variable
}

Nel punto di chiamata in è facoltativo: funzionano sia Trace(matrix) sia Trace(in matrix). Per int, double, DateTime e altri tipi piccoli, in non fa guadagnare nulla e può essere leggermente più lento; usalo per struct con diversi campi che passi spesso.

Restrizioni

  • I parametri ref e out non possono essere opzionali (CS1741). Un parametro in sì: static void Connect(in int retries = 3) è consentito.
  • Gli overload non possono differire solo tra ref e out (o in), perché a livello di runtime sono la stessa cosa. F(int x) e F(ref int x) possono convivere.
  • I metodi async e gli iteratori (metodi che usano yield return) non possono avere parametri ref, out o in. Da quei metodi restituisci invece una tupla o un oggetto risultato.
  • Una lambda o una funzione locale non può catturare un parametro ref, out o in del metodo che la contiene; copialo prima in una variabile locale.

ref, out e in a confronto

Il chiamante deve assegnare primaIl metodo deve assegnareIl metodo può leggereParola chiave nella chiamata
(nessuno)sìnosì (una copia)nessuna
refsìnosìobbligatoria
outnosì, su ogni percorsosolo dopo aver assegnatoobbligatoria
insìnon consentitosìfacoltativa

Domande frequenti

Che differenza c'è tra ref e out in C#?

Entrambi passano la variabile stessa del chiamante invece di una copia. Con ref, la variabile deve essere assegnata prima della chiamata e il metodo può leggerla e modificarla. Con out, la variabile non deve essere assegnata prima, il metodo non può leggerla prima di scriverla e deve assegnarla prima di restituire il controllo (altrimenti CS0177). out significa "questo è un risultato aggiuntivo".

Cosa significa out var in C#?

Da C# 7.0 puoi dichiarare la variabile out dentro la chiamata: if (int.TryParse(text, out var number)). Il tipo della variabile viene dedotto dal parametro, ed è visibile anche dopo l'istruzione. out _ scarta un risultato che non ti serve.

In C# gli oggetti vengono passati per riferimento?

No, ogni argomento viene passato per valore a meno che tu non scriva ref, out o in. Per una classe, il valore che viene copiato è un riferimento, quindi il metodo può modificare i campi dell'oggetto e il chiamante lo vede. Quello che non può fare senza ref è far puntare la variabile del chiamante a un oggetto diverso.

Cos'è il modificatore di parametro in in C#?

in (da C# 7.2) passa un argomento per riferimento ma in sola lettura: il metodo non può assegnarlo. Esiste per evitare di copiare struct grandi a ogni chiamata. Per tipi piccoli come int o DateTime non porta alcun vantaggio.

Perché ottengo "Argument 1 must be passed with the 'ref' keyword"?

Il metodo dichiara un parametro ref e la chiamata ha omesso la parola chiave. C# richiede ref (e out) anche nel punto di chiamata, così chi legge vede che la variabile può cambiare: Increment(ref count);, non Increment(count);.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA