Tuple in C#

Edoardo Midali
Edoardo Midali

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.