Menu

Tupla em C#: tuplas nomeadas, retornar vários valores, desconstrução

Uma tupla em C# agrupa alguns valores em um só sem declarar um tipo. Veja a sintaxe de value tuple, elementos nomeados, retornar vários valores de um método, desconstruir com var (a, b), tuplas como chave de dicionário e como ValueTuple difere do antigo System.Tuple.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

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>
Tipostruct (tipo de valor)classe (tipo de referência)
Nomes de elementosSimNão, só Item1, Item2
MutávelSim, os elementos são camposNão, propriedades somente leitura
DesconstruçãoSimSim, 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 Item1 e Item2, e o System.Text.Json escreve {} 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; deixa t1 como 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 em age, e como var infere 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.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR