Tipi di tupla (riferimenti per C#)

La funzionalità tuple fornisce una sintassi concisa per raggruppare più elementi di dati in una struttura di dati leggera. L'esempio seguente illustra come dichiarare una variabile di tupla, inizializzarla e accedere ai relativi membri dati:

(double, int) t1 = (4.5, 3);
Console.WriteLine($"Tuple with elements {t1.Item1} and {t1.Item2}.");
// Output:
// Tuple with elements 4.5 and 3.

(double Sum, int Count) t2 = (4.5, 3);
Console.WriteLine($"Sum of {t2.Count} elements is {t2.Sum}.");
// Output:
// Sum of 3 elements is 4.5.

Come illustrato nell'esempio precedente, per definire un tipo di tupla, specificare i tipi di tutti i relativi membri dati e, facoltativamente, i nomi dei campi. Non è possibile definire metodi in un tipo di tupla, ma è possibile usare i metodi forniti da .NET, come illustrato nell'esempio seguente:

(double, int) t = (4.5, 3);
Console.WriteLine(t.ToString());
Console.WriteLine($"Hash code of {t} is {t.GetHashCode()}.");
// Output:
// (4.5, 3)
// Hash code of (4.5, 3) is 718460086.

I tipi di tupla supportano operatori di uguaglianza == e !=. Per altre informazioni, vedere la sezione Uguaglianza delle tuple.

I tipi di tupla sono tipi valore; gli elementi della tupla sono campi pubblici. Ciò rende modificabili i tipi valore delle tuple.

È possibile definire tuple con un numero arbitrario elevato di elementi:

var t =
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18,
19, 20, 21, 22, 23, 24, 25, 26);
Console.WriteLine(t.Item26);  // output: 26

Casi d'uso di tuple

Uno dei casi d'uso più comuni delle tuple è come tipo restituito dal metodo. Ovvero, invece di definire i parametri out del metodo, è possibile raggruppare i risultati del metodo in un tipo restituito di tupla, come illustrato nell'esempio seguente:

int[] xs = new int[] { 4, 7, 9 };
var limits = FindMinMax(xs);
Console.WriteLine($"Limits of [{string.Join(" ", xs)}] are {limits.min} and {limits.max}");
// Output:
// Limits of [4 7 9] are 4 and 9

int[] ys = new int[] { -9, 0, 67, 100 };
var (minimum, maximum) = FindMinMax(ys);
Console.WriteLine($"Limits of [{string.Join(" ", ys)}] are {minimum} and {maximum}");
// Output:
// Limits of [-9 0 67 100] are -9 and 100

(int min, int max) FindMinMax(int[] input)
{
    if (input is null || input.Length == 0)
    {
        throw new ArgumentException("Cannot find minimum and maximum of a null or empty array.");
    }

    // Initialize min to MaxValue so every value in the input
    // is less than this initial value.
    var min = int.MaxValue;
    // Initialize max to MinValue so every value in the input
    // is greater than this initial value.
    var max = int.MinValue;
    foreach (var i in input)
    {
        if (i < min)
        {
            min = i;
        }
        if (i > max)
        {
            max = i;
        }
    }
    return (min, max);
}

Come illustrato nell'esempio precedente, è possibile usare l'istanza di tupla restituita direttamente o decostruirla in variabili separate.

È anche possibile usare tipi di tupla anziché tipi anonimi, ad esempio nelle query LINQ. Per altre informazioni, vedere Scelta tra tipi anonimi e tuple.

In genere, si usano tuple per raggruppare elementi di dati correlati in modo libero. Nelle API pubbliche è consigliabile definire una classe o un tipo di struttura.

Nomi dei campi della tupla

È possibile specificare in modo esplicito i nomi dei campi di tupla in un'espressione di inizializzazione della tupla o nella definizione di un tipo di tupla, come illustrato nell'esempio seguente:

var t = (Sum: 4.5, Count: 3);
Console.WriteLine($"Sum of {t.Count} elements is {t.Sum}.");

(double Sum, int Count) d = (4.5, 3);
Console.WriteLine($"Sum of {d.Count} elements is {d.Sum}.");

Se non si specifica un nome di campo, può essere dedotto dal nome della variabile corrispondente in un'espressione di inizializzazione della tupla, come illustrato nell'esempio seguente:

var sum = 4.5;
var count = 3;
var t = (sum, count);
Console.WriteLine($"Sum of {t.count} elements is {t.sum}.");

Questo è detto inizializzatori di proiezione di tupla. Il nome di una variabile non viene proiettato in un nome di campo di tupla nei casi seguenti:

  • Il nome candidato è un nome membro di un tipo di tupla, ad esempio Item3, ToString o Rest.
  • Quando il nome candidato è un duplicato di un altro nome di campo di tupla, implicito o esplicito.

Nei casi precedenti è possibile specificare in modo esplicito il nome di un campo o accedere a un campo in base al nome predefinito.

I nomi predefiniti dei campi della tupla sono Item1, Item2, Item3 e così via. È sempre possibile usare il nome predefinito di un campo, anche quando viene specificato un nome di campo in modo esplicito o dedotto, come illustrato nell'esempio seguente:

var a = 1;
var t = (a, b: 2, 3);
Console.WriteLine($"The 1st element is {t.Item1} (same as {t.a}).");
Console.WriteLine($"The 2nd element is {t.Item2} (same as {t.b}).");
Console.WriteLine($"The 3rd element is {t.Item3}.");
// Output:
// The 1st element is 1 (same as 1).
// The 2nd element is 2 (same as 2).
// The 3rd element is 3.

I confronti di uguaglianza tra tuple e assegnazione di tupla non prendono in considerazione i nomi dei campi.

In fase di compilazione, il compilatore sostituisce i nomi di campo non predefiniti con i nomi predefiniti corrispondenti. Di conseguenza, i nomi di campo specificati o dedotti in modo esplicito non sono disponibili in fase di esecuzione.

Suggerimento

Abilitare la regola di stile del codice .NET IDE0037 per impostare una preferenza sui nomi di campo di tupla dedotti o espliciti.

A partire da C# 12, è possibile specificare un alias per un tipo di tupla con una direttiva using. Nell'esempio seguente viene aggiunto un alias global using per un tipo di tupla con due valori integer per un valore Min e Max consentito:

global using BandPass = (int Min, int Max);

Dopo aver dichiarato l'alias, è possibile usare il nome BandPass come alias per il tipo di tupla:

BandPass bracket = (40, 100);
Console.WriteLine($"The bandpass filter is {bracket.Min} to {bracket.Max}");

Un alias non introduce un nuovo tipo, ma crea solo un sinonimo di un tipo esistente. È possibile decostruire una tupla dichiarata con l'alias BandPass uguale a quanto possibile con il tipo di tupla sottostante:

(int a , int b) = bracket;
Console.WriteLine($"The bracket is {a} to {b}");

Come per l'assegnazione o la decostruzione della tupla, non è necessario che i nomi dei membri della tupla corrispondano, ma i tipi sì.

Analogamente, un secondo alias con gli stessi tipi di membri e arity può essere usato in modo intercambiabile con l'alias originale. È possibile dichiarare un secondo alias:

using Range = (int Minimum, int Maximum);

È possibile assegnare una tupla Range a una tupla BandPass. Come per tutte le assegnazioni di tupla, non è necessario che i nomi dei campi corrispondano, devono corrispondere solo i tipi e l'arità.

Range r = bracket;
Console.WriteLine($"The range is {r.Minimum} to {r.Maximum}");

Un alias per un tipo di tupla fornisce altre informazioni semantiche quando si usano tuple. Non introduce un nuovo tipo. Per garantire l’indipendenza dai tipi, è invece necessario dichiarare un record posizionale.

Assegnazione e decostruzione della tupla

C# supporta l'assegnazione tra tipi di tupla che soddisfano entrambe le condizioni seguenti:

  • entrambi i tipi di tupla hanno lo stesso numero di elementi
  • per ogni posizione di tupla, il tipo dell'elemento della tupla di destra è uguale o convertibile in modo implicito nel tipo dell'elemento tupla di sinistra corrispondente

I valori degli elementi della tupla vengono assegnati seguendo l'ordine degli elementi della tupla. I nomi dei campi della tupla vengono ignorati e non assegnati, come illustrato nell'esempio seguente:

(int, double) t1 = (17, 3.14);
(double First, double Second) t2 = (0.0, 1.0);
t2 = t1;
Console.WriteLine($"{nameof(t2)}: {t2.First} and {t2.Second}");
// Output:
// t2: 17 and 3.14

(double A, double B) t3 = (2.0, 3.0);
t3 = t2;
Console.WriteLine($"{nameof(t3)}: {t3.A} and {t3.B}");
// Output:
// t3: 17 and 3.14

È anche possibile usare l'operatore di assegnazione = per decostruire un'istanza di tupla in variabili separate. Questa operazione può essere eseguita in molti modi:

  • Usare la parola chiave var all'esterno delle parentesi per dichiarare variabili tipizzate in modo implicito e consentire al compilatore di dedurre i relativi tipi:

    var t = ("post office", 3.6);
    var (destination, distance) = t;
    Console.WriteLine($"Distance to {destination} is {distance} kilometers.");
    // Output:
    // Distance to post office is 3.6 kilometers.
    
  • Dichiarare in modo esplicito il tipo di ogni variabile tra parentesi:

    var t = ("post office", 3.6);
    (string destination, double distance) = t;
    Console.WriteLine($"Distance to {destination} is {distance} kilometers.");
    // Output:
    // Distance to post office is 3.6 kilometers.
    
  • Dichiarare alcuni tipi in modo esplicito e altri tipi in modo implicito (con var) all'interno delle parentesi:

    var t = ("post office", 3.6);
    (var destination, double distance) = t;
    Console.WriteLine($"Distance to {destination} is {distance} kilometers.");
    // Output:
    // Distance to post office is 3.6 kilometers.
    
  • Usare le variabili esistenti:

    var destination = string.Empty;
    var distance = 0.0;
    
    var t = ("post office", 3.6);
    (destination, distance) = t;
    Console.WriteLine($"Distance to {destination} is {distance} kilometers.");
    // Output:
    // Distance to post office is 3.6 kilometers.
    

La destinazione di un'espressione di decostruzione può includere variabili esistenti e variabili dichiarate nella dichiarazione di decostruzione.

È anche possibile combinare la decostruzione con i criteri di ricerca per esaminare le caratteristiche dei campi in una tupla. Nell'esempio seguente si esegue un ciclo di diversi numeri interi e si stampano quelli che sono divisibili per 3. Si decostruisce il risultato della tupla di Int32.DivRem e lo si confronta con un valore Remainder pari a 0:

for (int i = 4; i < 20;  i++)
{
    if (Math.DivRem(i, 3) is ( Quotient: var q, Remainder: 0 ))
    {
        Console.WriteLine($"{i} is divisible by 3, with quotient {q}");
    }
}

Per altre informazioni sulla decostruzione delle tuple e di altri tipi, vedere Decostruzione di tuple e altri tipi.

Uguaglianza delle tuple

I tipi di tupla supportano gli operatori == e !=. Questi operatori confrontano i membri dell'operando di sinistra con i membri corrispondenti dell'operando di destra seguendo l'ordine degli elementi della tupla.

(int a, byte b) left = (5, 10);
(long a, int b) right = (5, 10);
Console.WriteLine(left == right);  // output: True
Console.WriteLine(left != right);  // output: False

var t1 = (A: 5, B: 10);
var t2 = (B: 5, A: 10);
Console.WriteLine(t1 == t2);  // output: True
Console.WriteLine(t1 != t2);  // output: False

Come illustrato nell'esempio precedente, le operazioni == e != non prendono in considerazione i nomi dei campi della tupla.

Due tuple sono confrontabili quando vengono soddisfatte entrambe le condizioni seguenti:

  • Entrambe le tuple hanno lo stesso numero di elementi. Ad esempio, t1 != t2 non viene compilata se t1 e t2 hanno numeri diversi di elementi.
  • Per ogni posizione di tupla, gli elementi corrispondenti degli operandi di tupla di sinistra e di destra sono confrontabili con gli operatori == e !=. Ad esempio, (1, (2, 3)) == ((1, 2), 3) non viene compilata perché 1 non è paragonabile a (1, 2).

Gli operatori == e != confrontano le tuple in modo corto circuito. Ovvero, un'operazione si arresta non appena soddisfa una coppia di elementi non uguali o raggiunge le estremità delle tuple. Tuttavia, prima di qualsiasi confronto, vengono valutati tutti gli elementi della tupla, come illustrato nell'esempio seguente:

Console.WriteLine((Display(1), Display(2)) == (Display(3), Display(4)));

int Display(int s)
{
    Console.WriteLine(s);
    return s;
}
// Output:
// 1
// 2
// 3
// 4
// False

Tuple come parametri out

In genere, si esegue il refactoring di un metodo con parametri out in un metodo che restituisce una tupla. Tuttavia, esistono casi in cui un parametro out può essere di un tipo di tupla. L'esempio seguente illustra come usare le tuple come parametri out:

var limitsLookup = new Dictionary<int, (int Min, int Max)>()
{
    [2] = (4, 10),
    [4] = (10, 20),
    [6] = (0, 23)
};

if (limitsLookup.TryGetValue(4, out (int Min, int Max) limits))
{
    Console.WriteLine($"Found limits: min is {limits.Min}, max is {limits.Max}");
}
// Output:
// Found limits: min is 10, max is 20

Confronto tra tuple e System.Tuple

Le tuple C#, supportate dai tipi System.ValueTuple, sono diverse dalle tuple rappresentate dai tipi System.Tuple. Le differenze principali sono le seguenti:

  • I tipi System.ValueTuple sono tipi valore. I tipi System.Tuple sono tipi di riferimento.
  • I tipi System.ValueTuple sono modificabili. I tipi System.Tuple non sono modificabili.
  • I membri dati dei tipi System.ValueTuple sono campi. I membri dati dei tipi System.Tuple sono proprietà.

Specifiche del linguaggio C#

Per altre informazioni, vedi:

Vedi anche