Uma tupla junta um número pequeno e fixo de valores em um único valor sem declarar uma classe para isso. ("Ana", 31) é uma tupla de uma string e um int. Tuplas são a forma idiomática de retornar duas ou três coisas de um método e de guardar pares de vida curta dentro de um método.
Criando uma tupla
O tipo também é escrito com parênteses: (string, int) é um tipo de tupla cujo primeiro elemento é uma string e o segundo um int.
Saída:
Ana is 31
(3, 4)
179.70
(Ana, 32)
Sem nomes, os elementos são Item1, Item2, Item3 e assim por diante. Essas tuplas são structs System.ValueTuple, então atribuir uma a outra variável copia os valores, e mudar a cópia não afeta o original.
Elementos nomeados
Item1 não diz nada sobre o que o valor é. Dê nomes aos elementos, no tipo ou no literal:
Saída:
Ana, 31
Desk lamp costs 24.99
Ana
Ana
Os nomes são uma conveniência de tempo de compilação. Em tempo de execução, as duas tuplas acima são valores ValueTuple<string, int> comuns, e é por isso que atribuir person a uma tupla com outros nomes compila: só os tipos dos elementos e a ordem deles precisam corresponder.
O C# 7.1 em diante também infere os nomes a partir das variáveis do literal, então var t = (name, age); dá t.name e t.age sem escrevê-los duas vezes. No C# 7.0 esses elementos são só Item1 e Item2.
Retornando vários valores de um método
Esse é o principal motivo de as tuplas terem sido adicionadas à linguagem. Antes delas, as opções eram parâmetros out, uma classe feita só para isso, ou System.Tuple com seu Item1 anônimo.
Saída:
Lowest 60, highest 95
Average 78.75
Os nomes dos elementos no tipo de retorno viram os nomes que quem chama vê. Compare com o mesmo método usando parâmetros out:
static void Stats(int[] scores, out int min, out int max, out double average) { ... }
Stats(scores, out int min, out int max, out double avg);
Parâmetros out continuam sendo a convenção para a forma do TryParse, em que um bool diz se funcionou e o valor volta junto. Para "calcular várias coisas e retorná-las", uma tupla se lê melhor. Veja ref e out.
Desconstrução
A desconstrução desempacota uma tupla em variáveis separadas em uma única instrução:
Saída:
Lima 24.5
Lima
24.5
2 1
Os nomes das variáveis em var (city, temp) são seus; eles não precisam corresponder aos nomes dos elementos da tupla, porque a desconstrução é por posição. A troca (a, b) = (b, a) monta primeiro uma tupla com os valores antigos e depois atribui, então nenhum valor se perde.
Suas próprias classes podem suportar desconstrução declarando um método Deconstruct com parâmetros out. A partir do C# 7 com .NET Core 2.0 ou posterior, KeyValuePair também tem um, então um laço sobre um dicionário pode desempacotar cada entrada no cabeçalho:
class Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) { X = x; Y = y; }
public void Deconstruct(out int x, out int y) { x = X; y = Y; }
}
var (x, y) = new Point(3, 4);
foreach (var (name, score) in scoresByName) // Dictionary<string, int>
{
Console.WriteLine($"{name}: {score}");
}
A mesma forma foreach (var (a, b) in listOfTuples) funciona para uma lista de tuplas. Usar a tupla inteira como uma variável do laço e ler os elementos nomeados, como a próxima seção faz, funciona igualmente bem.
Tuplas em listas, LINQ e dicionários
Uma lista de tuplas nomeadas é uma forma leve de guardar linhas de dados dentro de um método:
Saída:
Chloe 95
Ana 88
Ben 72
(Ana, B), (Chloe, A)
Ben
False
ValueTuple implementa Equals e GetHashCode elemento a elemento, então (2, 7) montado em dois lugares diferentes encontra a mesma entrada do dicionário. Isso faz de uma tupla a chave composta mais simples: sem classe, sem função de hash escrita à mão, sem concatenação de strings como row + ":" + col.
Igualdade de tuplas
Equals compara elemento a elemento e funciona desde que as tuplas foram introduzidas:
Saída:
True
False
O C# 7.3 adicionou == e != para tuplas, que compilam para a mesma comparação elemento a elemento e também ignoram os nomes:
if ((order.Status, order.Paid) == ("shipped", true)) { ... }
Console.WriteLine((1, "x") == (1, "x")); // True
System.Tuple vs ValueTuple
O .NET 4 introduziu System.Tuple, uma classe criada com Tuple.Create. Ela ainda está no framework e ainda aparece em código e APIs mais antigos, e é por isso que buscas por "C# tuple" muitas vezes caem nela.
ValueTuple (C# 7) | System.Tuple (.NET 4) | |
|---|---|---|
| Sintaxe | (1, "a"), (int, string) | Tuple.Create(1, "a"), Tuple<int, string> |
| Tipo | struct (tipo de valor) | classe (tipo de referência) |
| Nomes de elementos | Sim | Não, só Item1, Item2 |
| Mutável | Sim, os elementos são campos | Não, propriedades somente leitura |
| Desconstrução | Sim | Sim, por métodos de extensão |
Saída:
Ben 25
(Ben, 25)
ToValueTuple() e ToTuple() convertem entre os dois. Prefira value tuples em código novo: elas não alocam nada no heap e podem ter nomes.
Quando usar uma classe ou um record
Tuplas são melhores quando o agrupamento é local e óbvio: um método retorna dois números, uma consulta LINQ leva um par para o passo seguinte, um dicionário precisa de uma chave de duas partes. Troque para um tipo com nome quando:
- A mesma forma aparece em várias assinaturas de métodos públicos.
(string, string, int)em cinco lugares é uma classe que ainda não foi escrita. - Os valores precisam de comportamento, validação, ou de mais de três ou quatro elementos.
- Os dados cruzam uma fronteira como a serialização. Os nomes não existem em tempo de execução, então o Newtonsoft.Json escreve
Item1eItem2, e oSystem.Text.Jsonescreve{}porque os elementos da tupla são campos, que ele ignora por padrão.
Os records do C# 9 dão a maior parte da conveniência de uma tupla com um nome de verdade. record Score(string Name, int Points); já vem com igualdade por valor, um ToString legível e desconstrução. Veja records.
Erros comuns
- Esperar que os nomes existam em tempo de execução. Reflection e serializadores veem
Item1,Item2(ou nada). - Alterar uma cópia. Tuplas são structs:
var t2 = t1; t2.Item1 = 5;deixat1como estava. - Tuplas com muitos elementos. Passando de três ou quatro, um tipo com nome se lê melhor.
- Confundir a ordem da desconstrução. Ela é por posição: com uma tupla
(Name, Age),var (age, name) = person;coloca o nome emage, e comovarinfere os tipos, nada avisa você.
Perguntas frequentes
Como retornar vários valores de um método em C#?
Retorne uma tupla: declare o tipo de retorno como (int Min, int Max) e faça return (lowest, highest);. Quem chama lê result.Min e result.Max, ou desconstrói com var (min, max) = MinMax(data);. As alternativas são parâmetros out, que combinam com o padrão TryParse, ou uma classe pequena ou um record quando o resultado tem um significado próprio.
O que é uma tupla nomeada em C#?
Uma tupla cujos elementos têm nomes em vez de Item1, Item2: (string Name, int Age) person = ("Ana", 31); permite escrever person.Name. Os nomes só existem em tempo de compilação; por baixo continua sendo um ValueTuple<string, int>, e Item1 continua funcionando.
Qual a diferença entre Tuple e ValueTuple em C#?
System.Tuple (do .NET 4) é uma classe: alocada no heap, imutável, e os elementos são sempre só Item1, Item2. System.ValueTuple (C# 7) é uma struct com campos mutáveis, suporta nomes de elementos e a sintaxe (a, b), e é o que todo literal de tupla cria. Use value tuples em código novo.
Como funciona a desconstrução de tuplas em C#?
var (name, age) = person; declara duas variáveis e atribui a elas os elementos da tupla em ordem. Você também pode atribuir a variáveis existentes com (a, b) = (b, a), o que as troca, e pular elementos com o descarte _: var (_, age) = person;.
Posso comparar tuplas com == em C#?
Desde o C# 7.3, == e != comparam tuplas elemento a elemento: (1, "a") == (1, "a") é true. Os nomes são ignorados; só as posições e os valores importam. Antes do C# 7.3, use Equals, que as value tuples implementam da mesma forma.