Tipos de tupla (Referência do C#)

O recurso de tuplas fornece sintaxe concisa para agrupar vários elementos de dados em uma estrutura de dados leve. O exemplo a seguir mostra como você pode declarar uma variável de tupla, inicializá-la e acessar os membros de dados:

(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.

Conforme mostrado no exemplo anterior, para definir um tipo de tupla, especifique os tipos de todos os membros de dados e, opcionalmente, os nomes de campo. Você não pode definir métodos em um tipo de tupla, mas pode usar os métodos fornecidos pelo .NET, conforme mostrado no exemplo a seguir:

(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.

Os tipos de tupla dão suporte a operadores de igualdade == e !=. Para obter mais informações, confira a seção Igualdade de tupla.

Os tipos de tupla são tipos de valor. Os elementos de tupla são campos públicos. Isso torna as tuplas tipos de valor mutáveis.

Você pode definir as tuplas com um grande número arbitrário de elementos:

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

Casos de uso das tuplas

Um dos caos de uso mais comuns das tuplas é como tipo de retorno do método. Ou seja, em vez de definir os parâmetros do método out, você pode agrupar os resultados do método em um tipo de retorno de tupla, conforme mostrado no exemplo a seguir:

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);
}

Conforme mostrado no exemplo anterior, você pode trabalhar diretamente com a instância de tupla retornada ou desconstruí-la em variáveis diferentes.

Você também pode usar tipos de tupla, em vez de tipos anônimos, por exemplo, em consultas LINQ. Para obter mais informações, confira Como escolher entre tipos anônimos e tipos de tupla.

Normalmente, você usa as tuplas para agrupar elementos de dados ligeiramente relacionados. Em APIs públicas, defina uma classe ou um tipo de estrutura.

Nomes de campo de tupla

Você especifica explicitamente os nomes de campos de tupla em uma expressão de inicialização de tupla ou na definição de um tipo de tupla, conforme mostrado no exemplo a seguir:

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 você não especificar um nome de campo, ele poderá ser inferido pelo nome da variável correspondente em uma expressão de inicialização de tupla, conforme mostrado no exemplo a seguir:

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

Isso é chamado de inicializadores de projeção de tupla. O nome de uma variável não é projetado em um nome de campo de tupla nos seguintes casos:

  • O nome candidato é um nome membro de um tipo de tupla, por exemplo, Item3, ToString ou Rest.
  • O nome candidato é uma duplicata de outro nome de campo de tupla, explícito ou implícito.

Nos casos anteriores, especifique explicitamente o nome de um campo ou acesse um campo pelo nome padrão.

Os nomes padrão de campos de tupla sãoItem1, Item2, Item3 e assim por diante. Você sempre pode usar o nome padrão de um campo, mesmo quando um nome de campo for especificado explicitamente ou inferido, conforme mostrado no exemplo a seguir:

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.

As comparações de atribuição de tupla e igualdade de tupla não levam em conta os nomes de campo.

No tempo de compilação, o compilador substitui os nomes de campo não padrão pelos nomes padrão correspondentes. Como resultado, os nomes de campo especificados explicitamente ou inferidos não ficam disponíveis no tempo de execução.

Dica

Habilite a regra de estilo de código do .NET IDE0037 para definir uma preferência por nomes de campo de tupla inferidos ou explícitos.

Começando no C# 12, você pode especificar um alias para um tipo de tupla com uma diretiva using. O seguinte exemplo adiciona um alias global using para um tipo de tupla com dois valores inteiros para um valor permitido Min e Max:

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

Depois de declarar o alias, você pode usar o nome BandPass como um alias para esse tipo de tupla:

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

Um alias não introduz um novo tipo, mas cria apenas um sinônimo para um tipo existente. Você pode desconstruir uma tupla declarada com o alias BandPass da mesma forma que pode fazer com o tipo de tupla subjacente:

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

Assim como na atribuição ou desconstrução de tupla, os nomes de membro de tupla não precisam corresponder; os tipos precisam.

Da mesma forma, um segundo alias com os mesmos tipos de paridade e membro pode ser usado de modo intercambiável com o alias original. Você pode declarar um segundo alias:

using Range = (int Minimum, int Maximum);

Você pode atribuir uma tupla Range a uma tupla BandPass. Assim como acontece com toda a atribuição de tupla, os nomes de campo não precisam corresponder, apenas os tipos e a paridade.

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

Um alias para um tipo de tupla fornece mais informações semânticas quando você usa tuplas. Ele não introduz um novo tipo. Para fornecer segurança de tipo, você deve declarar um record posicional.

Atribuição e desconstrução de tupla

O C# dá suporte à atribuição entre tipos de tupla que atendem às duas seguintes condições a seguir:

  • ambos os tipos de tupla têm o mesmo número de elementos
  • para cada posição de tupla, o tipo do elemento de tupla à direita é o mesmo ou pode ser convertido implicitamente no tipo do elemento de tupla à esquerda correspondente

Os valores do elemento de tupla são atribuídos seguindo a ordem dos elementos de tupla. Os nomes de campos de tupla são ignorados e não atribuídos, conforme mostrado no exemplo a seguir:

(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

Você também pode usar o operador de atribuição = para desconstruir uma instância de tupla em variáveis diferentes. Você pode fazer isso de várias maneiras:

  • Use a palavra-chave var fora dos parênteses para declarar variáveis digitadas implicitamente e permita que o compilador infira os tipos:

    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.
    
  • Declare explicitamente o tipo de cada campo entre parênteses:

    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.
    
  • Declare alguns tipos explicitamente e outros tipos implicitamente (com var) dentro dos parênteses:

    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.
    
  • Use as variáveis existentes:

    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.
    

O destino de uma expressão de desconstrução pode incluir variáveis existentes e variáveis declaradas na declaração de desconstrução.

Você também pode combinar a desconstrução com padrões correspondentes para inspecionar as características dos campos em uma tupla. O exemplo a seguir realiza loops em vários inteiros e imprime aqueles que são divisíveis por 3. Ele desconstrói o resultado da tupla de Int32.DivRem e faz a correspondência com um Remainder de 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}");
    }
}

Para obter mais informações sobre a desconstrução de tuplas e outros tipos, confira Desconstrução de tuplas e outros tipos.

Igualdade de tupla

Os tipos de tupla dão suporte aos == operadores e !=. Esses operadores comparam membros do operando à esquerda com os membros correspondentes do operando à direita, seguindo a ordem dos elementos de 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

Conforme mostrado no exemplo anterior, as operações == e != não levam em conta os nomes de campo de tupla.

Duas tuplas são comparáveis quando ambas as seguintes condições são atendidas:

  • Ambas as tuplas têm o mesmo número de elementos. Por exemplo, t1 != t2 não é compilado, se t1 e t2 tiverem números diferentes de elementos.
  • Para cada posição de tupla, os elementos correspondentes dos operandos de tupla à esquerda e à direita são comparáveis aos operadores == e !=. Por exemplo, (1, (2, 3)) == ((1, 2), 3) não é compilado porque 1 não é comparável a (1, 2).

Os operadores == e != comparam as tuplas em curto-circuito. Ou seja, uma operação é interrompida assim que atende a um par de elementos diferentes ou que atinge as extremidades das tuplas. No entanto, antes de qualquer comparação, todos os elementos de tupla são avaliados, conforme mostrado no exemplo a seguir:

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

Tuplas como parâmetros de saída

Normalmente, você refatora um método que tem parâmetros out em um método que retorna uma tupla. No entanto, há casos em que um parâmetro out pode ser de um tipo de tupla. O exemplo a seguir mostra como trabalhar com tuplas como parâmetros 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

Tuplas versus System.Tuple

As tuplas do C#, que são respaldadas por tipos System.ValueTuple, são diferentes das tuplas representadas por tipos System.Tuple. As principais diferenças são as seguintes:

  • Os tipos System.ValueTuple são tipos de valor. Os System.Tuple tipos são tipos de referência.
  • Os tipos System.ValueTuple são mutáveis. Os tipos System.Tuple são imutáveis.
  • Os membros de dados dos tipos System.ValueTuple são campos. Os membros de dados dos tipos System.Tuple são propriedades.

Especificação da linguagem C#

Para obter mais informações, consulte:

Confira também