Tuple in C#

Le Tuple in C# sono strutture dati che consentono di raggruppare più valori di tipi diversi in un’unica entità. Introdotte per la prima volta in .NET Framework 4.0 e significativamente migliorate in C# 7.0 con le ValueTuple, rappresentano un modo elegante e performante per gestire gruppi di dati correlati senza dover creare classi o struct dedicate.
Evoluzione delle Tuple in C#
C# offre due implementazioni principali di tuple:
- Tuple (Reference Type): Introdotte in .NET 4.0, basate su classi
- ValueTuple (Value Type): Introdotte in C# 7.0, basate su struct
Tuple Classiche (System.Tuple)
Creazione e Utilizzo Base
// Creazione con factory method
var persona = Tuple.Create("Mario", "Rossi", 30);
// Creazione con costruttore
var coordinate = new Tuple<double, double>(45.4642, 9.1900);
// Accesso agli elementi tramite proprietà Item
Console.WriteLine($"Nome: {persona.Item1}");
Console.WriteLine($"Cognome: {persona.Item2}");
Console.WriteLine($"Età: {persona.Item3}");
Tuple con Più Elementi
// Tuple con 7 elementi (massimo supportato direttamente)
var datiCompleti = Tuple.Create("Mario", "Rossi", 30, "Milano",
"Ingegnere", 50000.0, DateTime.Now);
// Per più di 7 elementi, si usa Tuple<T1,T2,T3,T4,T5,T6,T7,TRest>
var tupleEstesa = new Tuple<string, string, int, string, string, double, DateTime,
Tuple<string, bool>>(
"Mario", "Rossi", 30, "Milano", "Ingegnere", 50000.0, DateTime.Now,
Tuple.Create("Note aggiuntive", true)
);
ValueTuple (C# 7.0+)
Sintassi e Creazione
// Sintassi con parentesi (più concisa)
var persona = ("Mario", "Rossi", 30);
// Con tipi espliciti
(string nome, string cognome, int età) personaNominata = ("Mario", "Rossi", 30);
// Assegnazione con nomi dei campi
var coordinate = (X: 45.4642, Y: 9.1900);
// Accesso agli elementi
Console.WriteLine($"Nome: {persona.Item1}");
Console.WriteLine($"X: {coordinate.X}, Y: {coordinate.Y}");
Tuple con Nomi dei Campi
// Definizione con nomi significativi
var prodotto = (
Nome: "Laptop",
Prezzo: 1299.99m,
Categoria: "Elettronica",
Disponibile: true
);
// Accesso tramite nomi
Console.WriteLine($"Prodotto: {prodotto.Nome}");
Console.WriteLine($"Prezzo: €{prodotto.Prezzo}");
Console.WriteLine($"Disponibile: {prodotto.Disponibile}");
Tuple come Valori di Ritorno
Funzioni che Restituiscono Tuple
// Metodo che restituisce coordinate
public static (double Latitudine, double Longitudine) GetCoordinate(string città)
{
return città.ToLower() switch
{
"milano" => (45.4642, 9.1900),
"roma" => (41.9028, 12.4964),
"napoli" => (40.8518, 14.2681),
_ => (0.0, 0.0)
};
}
// Utilizzo
var (lat, lon) = GetCoordinate("Milano");
Console.WriteLine($"Milano: {lat}, {lon}");
Metodi con Risultati Multipli
public static (bool Successo, string Messaggio, int Risultato) EseguiOperazione(int a, int b)
{
try
{
if (b == 0)
return (false, "Divisione per zero non consentita", 0);
int risultato = a / b;
return (true, "Operazione completata", risultato);
}
catch (Exception ex)
{
return (false, ex.Message, 0);
}
}
// Utilizzo con decostruzione
var (successo, messaggio, risultato) = EseguiOperazione(10, 2);
if (successo)
{
Console.WriteLine($"Risultato: {risultato}");
}
else
{
Console.WriteLine($"Errore: {messaggio}");
}
Decostruzione delle Tuple
Decostruzione Base
var persona = ("Mario", "Rossi", 30);
// Decostruzione in variabili separate
var (nome, cognome, età) = persona;
// Decostruzione parziale con discard
var (nome2, _, età2) = persona; // Ignora il cognome
// Decostruzione con var implicito
(var n, var c, var e) = persona;
Decostruzione in Oggetti Personalizzati
public class Persona
{
public string Nome { get; set; }
public string Cognome { get; set; }
public int Età { get; set; }
// Metodo di decostruzione personalizzato
public void Deconstruct(out string nome, out string cognome, out int età)
{
nome = Nome;
cognome = Cognome;
età = Età;
}
// Decostruzione alternativa
public void Deconstruct(out string nomeCompleto, out int età)
{
nomeCompleto = $"{Nome} {Cognome}";
età = Età;
}
}
// Utilizzo
var persona = new Persona { Nome = "Mario", Cognome = "Rossi", Età = 30 };
var (nome, cognome, età) = persona;
var (nomeCompleto, età2) = persona;
Tuple in Collezioni
Liste e Array di Tuple
// Lista di tuple per rappresentare punti
var punti = new List<(double X, double Y)>
{
(1.0, 2.0),
(3.5, 4.8),
(7.2, 1.9)
};
// Iterazione su tuple
foreach (var (x, y) in punti)
{
Console.WriteLine($"Punto: ({x}, {y})");
}
// Dizionario con tuple come valore
var studenti = new Dictionary<string, (string Corso, int Voto, DateTime Data)>
{
["S001"] = ("Matematica", 85, DateTime.Now.AddDays(-10)),
["S002"] = ("Fisica", 92, DateTime.Now.AddDays(-5)),
["S003"] = ("Chimica", 78, DateTime.Now.AddDays(-3))
};
// Accesso agli elementi
var (corso, voto, data) = studenti["S001"];
Console.WriteLine($"Corso: {corso}, Voto: {voto}");
Confronto e Uguaglianza
Confronto tra Tuple
var tuple1 = (Nome: "Mario", Età: 30);
var tuple2 = (Nome: "Mario", Età: 30);
var tuple3 = (Nome: "Luigi", Età: 25);
// Confronto di uguaglianza
Console.WriteLine(tuple1 == tuple2); // True
Console.WriteLine(tuple1 == tuple3); // False
// Confronto con Equals
Console.WriteLine(tuple1.Equals(tuple2)); // True
// GetHashCode
Console.WriteLine(tuple1.GetHashCode());
Ordinamento di Tuple
var persone = new List<(string Nome, int Età)>
{
("Alice", 25),
("Bob", 30),
("Charlie", 20),
("Diana", 35)
};
// Ordinamento per età
persone.Sort((x, y) => x.Età.CompareTo(y.Età));
// Con LINQ
var personeOrdinate = persone.OrderBy(p => p.Nome).ToList();
foreach (var (nome, età) in personeOrdinate)
{
Console.WriteLine($"{nome}: {età} anni");
}
Applicazioni Pratiche
Parsing e Validazione
public static (bool IsValid, int Value, string Error) TryParseInt(string input)
{
if (string.IsNullOrWhiteSpace(input))
return (false, 0, "Input vuoto o null");
if (int.TryParse(input, out int result))
return (true, result, null);
return (false, 0, "Formato non valido");
}
// Utilizzo
var input = "123";
var (isValid, value, error) = TryParseInt(input);
if (isValid)
{
Console.WriteLine($"Valore parsato: {value}");
}
else
{
Console.WriteLine($"Errore: {error}");
}
Coordinate e Geometria
public class GeometriaHelper
{
public static (double Distanza, double Angolo) CalcolaDistanzaEAngolo(
(double X, double Y) punto1,
(double X, double Y) punto2)
{
double dx = punto2.X - punto1.X;
double dy = punto2.Y - punto1.Y;
double distanza = Math.Sqrt(dx * dx + dy * dy);
double angolo = Math.Atan2(dy, dx) * 180.0 / Math.PI;
return (distanza, angolo);
}
public static (double Area, double Perimetro) CalcolaRettangolo(
double larghezza, double altezza)
{
double area = larghezza * altezza;
double perimetro = 2 * (larghezza + altezza);
return (area, perimetro);
}
}
// Utilizzo
var p1 = (X: 0.0, Y: 0.0);
var p2 = (X: 3.0, Y: 4.0);
var (distanza, angolo) = GeometriaHelper.CalcolaDistanzaEAngolo(p1, p2);
var (area, perimetro) = GeometriaHelper.CalcolaRettangolo(5.0, 3.0);
Pattern Matching con Tuple
public static string DescriviPunto((int X, int Y) punto)
{
return punto switch
{
(0, 0) => "Origine",
(0, _) => "Sull'asse Y",
(_, 0) => "Sull'asse X",
(var x, var y) when x == y => "Sulla diagonale principale",
(var x, var y) when x == -y => "Sulla diagonale secondaria",
(var x, var y) when x > 0 && y > 0 => "Primo quadrante",
(var x, var y) when x < 0 && y > 0 => "Secondo quadrante",
(var x, var y) when x < 0 && y < 0 => "Terzo quadrante",
_ => "Quarto quadrante"
};
}
// Test
Console.WriteLine(DescriviPunto((0, 0))); // Origine
Console.WriteLine(DescriviPunto((3, 3))); // Sulla diagonale principale
Console.WriteLine(DescriviPunto((2, -2))); // Sulla diagonale secondaria
Performance e Best Practices
Confronto Performance
// ValueTuple (struct) - più performante
(int, string) valueTuple = (42, "Test");
// Tuple (class) - allocazione heap
Tuple<int, string> referenceTuple = Tuple.Create(42, "Test");
// Benchmark semplificato
var stopwatch = Stopwatch.StartNew();
// ValueTuple operations
for (int i = 0; i < 1000000; i++)
{
var vt = (i, $"Value{i}");
var sum = vt.Item1 + vt.Item2.Length;
}
stopwatch.Stop();
Console.WriteLine($"ValueTuple time: {stopwatch.ElapsedMilliseconds}ms");
Best Practices
// ✅ Usa nomi significativi per i campi
var risultato = (Successo: true, Valore: 42, Messaggio: "OK");
// ❌ Evita tuple troppo complesse
// var datiComplessi = (string, int, double, bool, DateTime, List<string>, Dictionary<int, string>);
// ✅ Preferisci tuple per dati temporanei
public (bool Found, User User) FindUser(int id)
{
// Implementazione
return (true, new User());
}
// ✅ Usa decostruzione per chiarezza
var (found, user) = FindUser(123);
if (found)
{
// Usa user
}
// ✅ Considera struct personalizzate per dati complessi o riutilizzabili
public readonly struct Point
{
public double X { get; }
public double Y { get; }
public Point(double x, double y) => (X, Y) = (x, y);
public void Deconstruct(out double x, out double y) => (x, y) = (X, Y);
}
Limitazioni e Considerazioni
Limitazioni delle Tuple
// Reflection limitata con nomi dei campi ValueTuple
var tuple = (Nome: "Test", Valore: 42);
var type = tuple.GetType();
// I nomi dei campi non sono disponibili a runtime senza attributi speciali
// Serializzazione
// Le tuple potrebbero non serializzarsi come expected in JSON
// Considera classi per API pubbliche
Quando NON Usare Tuple
// ❌ Per API pubbliche durature
public (string, int, bool) MetodoAPIComplesso() // Difficile da comprendere
// ✅ Meglio una classe/record
public class RisultatoAPI
{
public string Messaggio { get; set; }
public int Codice { get; set; }
public bool Successo { get; set; }
}
// ❌ Per dati con logica complessa
// ✅ Preferisci struct o classi con metodi
Conclusione
Le Tuple in C# rappresentano uno strumento potente e flessibile per raggruppare dati correlati in modo temporaneo e leggero. Con l’introduzione delle ValueTuple in C# 7.0, offrono prestazioni eccellenti e una sintassi pulita. Sono ideali per valori di ritorno multipli, dati temporanei e situazioni dove creare una classe dedicata sarebbe eccessivo. Tuttavia, per strutture dati complesse, durature o con logica associata, è preferibile utilizzare classi, struct o record appropriati.