Una tupla raggruppa un numero piccolo e fisso di valori in un solo valore, senza dichiarare una classe apposta. ("Ana", 31) è una tupla formata da una string e un int. Le tuple sono il modo idiomatico per restituire due o tre cose da un metodo e per tenere coppie di breve durata dentro un metodo.
Creare una tupla
Anche il tipo si scrive con le parentesi: (string, int) è un tipo tupla il cui primo elemento è una string e il secondo un int.
Output:
Ana is 31
(3, 4)
179.70
(Ana, 32)
Senza nomi, gli elementi sono Item1, Item2, Item3 e così via. Queste tuple sono struct System.ValueTuple, quindi assegnarne una a un'altra variabile copia i valori, e modificare la copia lascia intatto l'originale.
Elementi con nome
Item1 non dice nulla su cosa sia il valore. Dai un nome agli elementi, nel tipo oppure nel letterale:
Output:
Ana, 31
Desk lamp costs 24.99
Ana
Ana
I nomi sono una comodità di compilazione. A runtime entrambe le tuple qui sopra sono semplici valori ValueTuple<string, int>, ed è per questo che assegnare person a una tupla con nomi diversi compila: devono corrispondere solo i tipi degli elementi e il loro ordine.
C# 7.1 e successivi deducono anche i nomi dalle variabili del letterale, quindi var t = (name, age); ti dà t.name e t.age senza doverli scrivere due volte. In C# 7.0 quegli elementi sono solo Item1 e Item2.
Restituire più valori da un metodo
È il motivo principale per cui le tuple sono state aggiunte al linguaggio. Prima le opzioni erano i parametri out, una classe usa e getta, o System.Tuple con i suoi anonimi Item1.
Output:
Lowest 60, highest 95
Average 78.75
I nomi degli elementi nel tipo di ritorno diventano i nomi che vede chi chiama. Confronta lo stesso metodo con i parametri out:
static void Stats(int[] scores, out int min, out int max, out double average) { ... }
Stats(scores, out int min, out int max, out double avg);
I parametri out restano la convenzione per la forma TryParse, dove un bool dice se l'operazione è riuscita e il valore torna indietro accanto a esso. Per "calcola diverse cose e restituiscile", una tupla si legge meglio. Vedi ref e out.
Decostruzione
La decostruzione spacchetta una tupla in variabili separate con una sola istruzione:
Output:
Lima 24.5
Lima
24.5
2 1
I nomi delle variabili in var (city, temp) li scegli tu; non devono coincidere con i nomi degli elementi della tupla, perché la decostruzione procede per posizione. Lo scambio (a, b) = (b, a) costruisce prima una tupla con i vecchi valori e poi assegna, quindi nessuno dei due valori va perso.
Anche le tue classi possono supportare la decostruzione dichiarando un metodo Deconstruct con parametri out. Da C# 7 con .NET Core 2.0 o successivi ne ha uno anche KeyValuePair, quindi un ciclo su un dizionario può spacchettare ogni voce direttamente nella sua intestazione:
class Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) { X = x; Y = y; }
public void Deconstruct(out int x, out int y) { x = X; y = Y; }
}
var (x, y) = new Point(3, 4);
foreach (var (name, score) in scoresByName) // Dictionary<string, int>
{
Console.WriteLine($"{name}: {score}");
}
La stessa forma foreach (var (a, b) in listOfTuples) funziona per una lista di tuple. Prendere l'intera tupla come unica variabile del ciclo e leggerne gli elementi con nome, come fa la sezione successiva, funziona altrettanto bene.
Tuple in liste, LINQ e dizionari
Una lista di tuple con nome è un modo leggero per tenere righe di dati dentro un metodo:
Output:
Chloe 95
Ana 88
Ben 72
(Ana, B), (Chloe, A)
Ben
False
ValueTuple implementa Equals e GetHashCode elemento per elemento, quindi (2, 7) costruita in due punti diversi trova la stessa voce del dizionario. Questo rende la tupla la chiave composta più semplice: nessuna classe, nessuna funzione di hash scritta a mano, nessuna concatenazione di stringhe come row + ":" + col.
Uguaglianza delle tuple
Equals confronta elemento per elemento e funziona da quando esistono le tuple:
Output:
True
False
C# 7.3 ha aggiunto == e != per le tuple, che vengono compilati nello stesso confronto elemento per elemento e ignorano anch'essi i nomi:
if ((order.Status, order.Paid) == ("shipped", true)) { ... }
Console.WriteLine((1, "x") == (1, "x")); // True
System.Tuple vs ValueTuple
.NET 4 ha introdotto System.Tuple, una classe creata con Tuple.Create. È ancora nel framework e compare ancora in codice e API più vecchi, ed è per questo che le ricerche su "C# tuple" finiscono spesso lì.
ValueTuple (C# 7) | System.Tuple (.NET 4) | |
|---|---|---|
| Sintassi | (1, "a"), (int, string) | Tuple.Create(1, "a"), Tuple<int, string> |
| Tipo | struct (tipo valore) | classe (tipo riferimento) |
| Nomi degli elementi | Sì | No, solo Item1, Item2 |
| Modificabile | Sì, gli elementi sono campi | No, proprietà di sola lettura |
| Decostruzione | Sì | Sì, tramite metodi di estensione |
Output:
Ben 25
(Ben, 25)
ToValueTuple() e ToTuple() convertono dall'una all'altra. Nel codice nuovo preferisci le value tuple: non allocano nulla sull'heap e possono avere nomi.
Quando usare invece una classe o un record
Le tuple rendono al meglio quando il raggruppamento è locale e ovvio: un metodo restituisce due numeri, una query LINQ passa una coppia al passo successivo, un dizionario ha bisogno di una chiave in due parti. Passa a un tipo con nome quando:
- La stessa forma compare in diverse firme di metodi pubblici.
(string, string, int)in cinque posti è una classe che nessuno ha ancora scritto. - I valori hanno bisogno di comportamento, di validazione, o di più di tre o quattro elementi.
- I dati attraversano un confine come la serializzazione. I nomi non esistono a runtime, quindi Newtonsoft.Json scrive
Item1eItem2, eSystem.Text.Jsonscrive{}perché gli elementi delle tuple sono campi, che per impostazione predefinita salta.
I record di C# 9 ti danno gran parte della comodità di una tupla con un nome vero. record Score(string Name, int Points); ha già uguaglianza di valore, un ToString leggibile e la decostruzione. Vedi record.
Errori comuni
- Aspettarsi che i nomi sopravvivano a runtime. La reflection e i serializzatori vedono
Item1,Item2(o niente del tutto). - Modificare una copia. Le tuple sono struct:
var t2 = t1; t2.Item1 = 5;lasciat1invariata. - Tuple con molti elementi. Oltre tre o quattro, un tipo con nome si legge meglio.
- Confondere l'ordine della decostruzione. È posizionale: con una tupla
(Name, Age),var (age, name) = person;mette il nome inage, e siccomevardeduce i tipi, niente ti avvisa.
Domande frequenti
Come restituisco più valori da un metodo in C#?
Restituisci una tupla: dichiara il tipo di ritorno come (int Min, int Max) e scrivi return (lowest, highest);. Chi chiama legge result.Min e result.Max, oppure decostruisce con var (min, max) = MinMax(data);. Le alternative sono i parametri out, adatti al pattern TryParse, oppure una piccola classe o un record quando il risultato ha un significato proprio.
Cos'è una tupla con nome in C#?
Una tupla i cui elementi hanno dei nomi invece di Item1, Item2: (string Name, int Age) person = ("Ana", 31); ti permette di scrivere person.Name. I nomi esistono solo in fase di compilazione; sotto resta una ValueTuple<string, int>, e Item1 continua a funzionare.
Qual è la differenza tra Tuple e ValueTuple in C#?
System.Tuple (da .NET 4) è una classe: allocata sull'heap, immutabile, e i suoi elementi sono sempre e solo Item1, Item2. System.ValueTuple (C# 7) è una struct con campi modificabili, supporta i nomi degli elementi e la sintassi (a, b), ed è quello che crea ogni letterale di tupla. Nel codice nuovo usa le value tuple.
Come funziona la decostruzione delle tuple in C#?
var (name, age) = person; dichiara due variabili e assegna loro in ordine gli elementi della tupla. Puoi anche assegnare a variabili esistenti con (a, b) = (b, a), che le scambia, e saltare elementi con il discard _: var (_, age) = person;.
Posso confrontare le tuple con == in C#?
Da C# 7.3, == e != confrontano le tuple elemento per elemento: (1, "a") == (1, "a") è true. I nomi vengono ignorati; contano solo posizioni e valori. Prima di C# 7.3, usa Equals, che le value tuple implementano nello stesso modo.