Compartilhar via


Tipos de tupla (Referência do C#)

O recurso de tuplas fornece uma sintaxe concisa para agrupar vários elementos de dados em uma estrutura de dados leve.

A linguagem C# faz referência a documentos da versão mais recentemente lançada da linguagem C#. Ele também contém a documentação inicial para recursos em visualizações públicas para a próxima versão do idioma.

A documentação identifica qualquer recurso introduzido pela primeira vez nas três últimas versões do idioma ou nas versões prévias públicas atuais.

Dica

Para descobrir quando um recurso foi introduzido pela primeira vez em C#, consulte o artigo sobre o histórico de versão da linguagem C#.

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.

Como mostra o exemplo anterior, para definir um tipo de tupla, especifique os tipos de todos os seus 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. Esse design torna as tuplas tipos de valor mutáveis .

Você pode definir tuplas com um número arbitrariamente grande 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 para tuplas

Um dos casos de uso mais comuns para tuplas é como um tipo de retorno de método. Em vez de definir out parâmetros de método, o método de grupo resulta em um tipo de retorno de tupla, como mostra o 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, use tuplas para agrupar elementos de dados vagamente relacionados. Em APIs públicas, defina uma classe ou um tipo de estrutura.

Nomes de campo de tupla

Especifique explicitamente os nomes de campo de tupla em uma expressão de inicialização de tupla ou na definição de um tipo de tupla, como mostra o 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, o compilador poderá inferi-lo do nome da variável correspondente em uma expressão de inicialização de tupla, como mostra o exemplo a seguir:

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

Esse recurso é 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 dos campos de tupla são Item1, Item2e Item3assim 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 que ou implicitamente conversível para o tipo do elemento de tupla à esquerda correspondente.

Atribua valores de elemento de tupla seguindo a ordem dos elementos de tupla. O processo de atribuição ignora os nomes dos campos de tupla, como mostra o 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

Use o operador = de atribuição para desconstruir uma instância de tupla em variáveis separadas. 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

Como mostra o exemplo anterior, as operações e != as == operações 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 usando os 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 os != operadores comparam tuplas de maneira de 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, existem alguns casos em que um out parâmetro pode ser 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 C# usam System.ValueTuple tipos e diferem das tuplas que usam System.Tuple tipos. As diferenças principais são:

  • 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