Tuplas no C#

As tuplas no C# são estruturas de dados que armazenam vários valores, inclusive de tipos diferentes, em uma única variável. Neste artigo, vamos ver como criar, ler, nomear e usar tuplas em métodos, além de entender a diferença entre Tuple e ValueTuple.

O que são tuplas no C#

A principal vantagem de uma tupla é guardar múltiplos tipos ao mesmo tempo. Por exemplo, você pode ter uma string e um int juntos, sem declarar nenhuma classe.

Como o C# é uma linguagem fortemente tipada, essa possibilidade ajuda muito no dia a dia. Afinal, ela evita a criação de classes simples apenas para transportar dados de um ponto a outro.

var tuple = new Tuple<string, int>("My text", 7);

Existem duas famílias de tuplas no .NET. A primeira é a classe System.Tuple, que existe desde o .NET Framework 4.0 e é um tipo de referência.

A segunda é a struct System.ValueTuple, que é a base da sintaxe moderna com parênteses. Segundo a referência oficial dos tipos de tupla, sempre que você escreve (string, int), o compilador usa um ValueTuple.

A sintaxe com parênteses chegou no C# 7.0, junto com o suporte a nomes e desconstrução. Desde então, as tuplas no C# ganharam melhorias em quase todas as versões da linguagem.

Ao longo do artigo, vamos ver as duas formas. Em seguida, vamos entender por que a sintaxe moderna costuma ser a melhor escolha.

Criando tuplas com a classe Tuple

A primeira forma de criar tuplas é usar new Tuple<T1, T2>, assim como fazemos normalmente com nossos objetos. Nesse caso, você informa os tipos entre os sinais de menor e maior.

var tuple = new Tuple<string, int>("My text", 7);

A segunda forma é usar o método Tuple.Create, que funciona basicamente como um atalho para o construtor anterior. Além disso, ele infere os tipos a partir dos argumentos, então você nem precisa informá-los.

// Tipos explícitos
var first = Tuple.Create<string, int>("My text", 7);

// Tipos inferidos pelo compilador
var second = Tuple.Create("My text", 7);

As duas variáveis têm exatamente o mesmo tipo: Tuple<string, int>. Portanto, a versão com inferência é mais curta e igualmente segura.

Essa foi a única forma de criar tuplas no C# durante muitos anos. Por isso, ela ainda aparece em bibliotecas e projetos mais antigos, e vale a pena reconhecê-la ao ler código legado.

Lendo os valores de uma tupla

Sempre que você cria uma tupla sem nomes, o compilador expõe os valores como Item1, Item2 e assim por diante. No exemplo acima, Item1 é a string com o texto My text e Item2 é o int com o valor 7.

Para acessar cada valor, basta usar o ponto depois do nome da variável.

var tuple = Tuple.Create("My text", 7);

Console.WriteLine(tuple.Item1); // Exibe "My text"
Console.WriteLine(tuple.Item2); // Exibe 7

Esse padrão se repete por toda a tupla. Contudo, existe um detalhe importante quando a tupla cresce muito.

Tuplas no C# com mais de sete itens

A classe Tuple tem versões genéricas de um até oito parâmetros de tipo. O oitavo, porém, não é um item comum: ele se chama TRest e guarda outra tupla com os valores restantes.

var primes = Tuple.Create(2, 3, 5, 7, 11, 13, 17, 19);

Console.WriteLine(primes.Item7);      // Exibe 17
Console.WriteLine(primes.Rest.Item1); // Exibe 19

Ou seja, com a classe Tuple, o oitavo valor fica dentro da propriedade Rest. Com a sintaxe moderna de ValueTuple, por outro lado, o compilador esconde esse aninhamento e libera nomes como Item8, Item9 e até Item20.

var numbers = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

Console.WriteLine(numbers.Item10); // Exibe 10

Na prática, uma tupla com muitos itens é um sinal de que o dado merece um tipo próprio. Ainda assim, é bom saber como o recurso funciona por baixo.

Inferindo os tipos das tuplas no C#

Particularmente, não gosto de usar new Tuple<T> nem Tuple.Create() por alguns motivos. O primeiro deles é que o C# permite declarar tuplas de forma muito mais simples, com parênteses.

(string, int) tuple = ("My text", 7);

Console.WriteLine(tuple.Item1); // Exibe "My text"
Console.WriteLine(tuple.Item2); // Exibe 7

Apenas com (string, int), criamos uma tupla com esses dois valores. Com isso, o código fica bem menos verboso.

O mesmo vale para o uso do var na declaração da variável. Dado um conjunto de valores entre parênteses, o compilador já entende que se trata de uma tupla.

var tuple = ("My text", 7);

Console.WriteLine(tuple.Item1); // Exibe "My text"
Console.WriteLine(tuple.Item2); // Exibe 7

Essa é a forma mais comum de declarar tuplas no C# moderno. Ela também aceita inferência parcial, misturando var e tipos explícitos na desconstrução.

Repare que, aqui, a variável não é mais um Tuple<string, int>. Ela é um ValueTuple<string, int>, que tem comportamento diferente, como veremos mais adiante.

Nomeando os itens das tuplas no C#

O segundo motivo pelo qual prefiro a sintaxe com parênteses é a possibilidade de nomear os itens. Afinal, ler Item1 e Item2 não diz nada sobre o significado de cada valor.

Quando declaramos o tipo da tupla, podemos dar um nome a cada item, como no exemplo abaixo.

(string First, int Second) tuple = ("My text", 7);

Console.WriteLine(tuple.Item1);  // Exibe "My text"
Console.WriteLine(tuple.Item2);  // Exibe 7
Console.WriteLine(tuple.First);  // Exibe "My text"
Console.WriteLine(tuple.Second); // Exibe 7

Note que Item1 e Item2 continuam disponíveis, porque o compilador não os descarta. Porém, agora temos também First e Second, conforme nomeamos na declaração.

Também é possível nomear os itens no próprio valor, usando o var na declaração.

var city = (Name: "Sao Paulo", Population: 11_450_000);

Console.WriteLine($"{city.Name}: {city.Population}");

Nomes inferidos a partir de variáveis

Desde o C# 7.1, o compilador também infere o nome dos itens a partir das variáveis usadas na inicialização. Esse recurso se chama inicializador de projeção de tupla.

var sum = 4.5;
var count = 3;

var stats = (sum, count);

Console.WriteLine($"Sum of {stats.count} elements is {stats.sum}.");

Vale lembrar que esses nomes existem só em tempo de compilação. Em tempo de execução, o compilador troca tudo por Item1, Item2 e assim por diante.

Listas dentro de tuplas

Podemos usar listas como itens das tuplas, tanto List quanto arrays. Na verdade, qualquer objeto pode ocupar uma posição na tupla.

var tuple = (4.5, new List<string>());

tuple.Item2.Add("My text");
tuple.Item2.Add("One more");

foreach (var item in tuple.Item2)
    Console.WriteLine(item);

No exemplo acima, criamos uma tupla com um item double e um List<string>. Em seguida, adicionamos dois elementos à lista e exibimos cada um na tela.

O caminho inverso também funciona muito bem. Ou seja, você pode ter uma lista de tuplas e percorrê-la desconstruindo cada item.

List<(string Name, int Score)> results =
[
    ("Alice", 92),
    ("Bob", 87)
];

foreach (var (name, score) in results)
    Console.WriteLine($"{name}: {score}");

Tuplas no C# em métodos

As tuplas no C# aparecem com frequência na entrada e na saída de métodos. Isso acontece principalmente quando precisamos agrupar vários valores em um único parâmetro ou retorno.

Tuplas como parâmetros

No exemplo abaixo, temos uma função que imprime um nome completo na tela. O parâmetro name é uma tupla do tipo (string FirstName, string LastName).

SayMyName(("André", "Baltieri"));

void SayMyName((string FirstName, string LastName) name)
    => Console.WriteLine($"{name.FirstName} {name.LastName}");

Repare nos parênteses duplos na chamada. O par externo pertence à chamada do método, enquanto o par interno cria a tupla.

Esse formato é útil quando os valores andam sempre juntos. No entanto, na maioria dos casos, dois parâmetros comuns deixam a assinatura mais simples do que uma tupla.

Tuplas como retorno

Outro uso muito comum das tuplas é o retorno de métodos. Nesse caso, elas evitam criar uma classe apenas para devolver mais de um valor.

var name = WhatsMyName("André", "Baltieri");

Console.WriteLine(name.FirstName); // Imprime André
Console.WriteLine(name.LastName);  // Imprime Baltieri

(string FirstName, string LastName) WhatsMyName(string firstName, string lastName)
    => (firstName, lastName);

No exemplo acima, o método WhatsMyName recebe duas strings e retorna uma tupla. Depois da chamada, guardamos o resultado na variável name e acessamos cada valor com name.FirstName e name.LastName.

Antes das tuplas, a alternativa mais comum eram os parâmetros out. Por isso, a documentação oficial sugere refatorar métodos com out para retornar uma tupla sempre que possível.

Desconstruindo o retorno

Você também pode desconstruir a tupla diretamente em variáveis separadas. Assim, cada valor ganha um nome local logo na chamada.

var (firstName, lastName) = WhatsMyName("André", "Baltieri");

Console.WriteLine(firstName);
Console.WriteLine(lastName);

Se algum valor não interessa, use o descarte _ no lugar dele. Para entender esse mecanismo em outros tipos, veja o artigo sobre o Deconstruct no C#.

Tuplas no C# em dicionários e consultas

As tuplas no C# também funcionam muito bem como chave de dicionário. Como elas comparam os valores item a item, você ganha uma chave composta sem criar nenhuma classe.

var grid = new Dictionary<(int Row, int Column), string>
{
    [(0, 0)] = "Origin",
    [(1, 3)] = "Sensor A"
};

if (grid.TryGetValue((1, 3), out var label))
    Console.WriteLine(label); // Exibe "Sensor A"

Da mesma forma, uma tupla pode ser o valor do dicionário. Nesse caso, cada chave aponta para um pequeno conjunto de dados relacionados.

var sizes = new Dictionary<string, (int Min, int Max)>
{
    ["Small"] = (0, 50),
    ["Medium"] = (51, 100)
};

Console.WriteLine(sizes["Medium"].Max); // Exibe 100

Projeções com LINQ

Outro uso comum das tuplas no C# está nas consultas LINQ. Em vez de projetar um tipo anônimo, você pode projetar uma tupla nomeada e desconstruí-la no foreach.

var words = new[] { "apple", "blueberry", "cherry" };

foreach (var (word, length) in words.Select(w => (Word: w, Length: w.Length)))
    Console.WriteLine($"{word} has {length} letters.");

Contudo, essa troca vale para consultas em memória. Provedores como o Entity Framework Core dependem de árvores de expressão, que não aceitam a sintaxe de tupla, então ali o tipo anônimo continua sendo o caminho.

Se você quer explorar outros recursos de consulta, veja como utilizar o let no LINQ.

Pattern matching com tuplas no C#

As tuplas no C# combinam muito bem com o pattern matching. Com uma expressão switch, você avalia vários valores ao mesmo tempo de forma legível.

string GetShippingRule(bool isPremium, decimal total) => (isPremium, total) switch
{
    (true, _) => "Free shipping",
    (false, >= 200m) => "Free shipping",
    (false, _) => "Standard shipping"
};

Console.WriteLine(GetShippingRule(false, 250m)); // Exibe "Free shipping"

Nesse exemplo, a tupla (isPremium, total) existe só para a comparação. Assim, cada linha da expressão descreve uma regra completa, sem uma cadeia de if aninhados.

Também é possível usar padrões de propriedade com os nomes dos itens. Por exemplo, Math.DivRem(10, 3) is (Quotient: var q, Remainder: 0) testa se a divisão é exata e captura o quociente.

Boas práticas com tuplas

Algumas regras simples ajudam a manter o código claro quando você usa tuplas no C#. A primeira delas é nomear sempre os itens que atravessam métodos.

  • Prefira a sintaxe com parênteses ao new Tuple<T> e ao Tuple.Create.
  • Nomeie os itens em retornos e parâmetros, para não depender de Item1 e Item2.
  • Mantenha as tuplas pequenas, com dois a quatro itens.
  • Use o descarte _ para ignorar valores que não interessam.
  • Troque a tupla por um record quando o dado ganhar significado no domínio.

Além disso, evite tuplas em propriedades de classes que representam entidades. Nesse caso, um tipo nomeado comunica melhor a intenção e facilita a manutenção.

Comparando e copiando tuplas

Desde o C# 7.3, as tuplas suportam os operadores == e !=. A comparação acontece item a item, na ordem em que eles aparecem.

var left = (Name: "Widget", Quantity: 5);
var right = (Product: "Widget", Amount: 5);

Console.WriteLine(left == right); // True

Note que os nomes dos itens não influenciam o resultado. Afinal, o que importa são os valores e as posições.

Além disso, a expressão with funciona com tuplas a partir do C# 10. Ela cria uma cópia com alguns valores alterados, sem mexer na original.

var original = (Name: "Widget", Price: 19.99m);
var discounted = original with { Price = 14.99m };

Se quiser se aprofundar nesse recurso, confira o artigo sobre a expressão with no C#.

Tuple e ValueTuple

Agora vem a diferença mais importante. A classe Tuple é um tipo de referência, então suas instâncias ficam no heap e seus itens são propriedades somente leitura.

Já a struct ValueTuple é um tipo de valor, que costuma ficar na pilha quando usada como variável local. Além disso, seus itens são campos públicos e mutáveis.

// Tuple: imutável, tipo de referência
var reference = Tuple.Create("My text", 7);
// reference.Item2 = 8; // Erro de compilação

// ValueTuple: mutável, tipo de valor
var value = ("My text", 7);
value.Item2 = 8; // Compila normalmente

// Criação explícita, equivalente à sintaxe com parênteses
var explicitTuple = new ValueTuple<string, int>("My text", 7);

Na prática, isso significa que alterar um Tuple exige criar um novo objeto. Por outro lado, um ValueTuple aceita a alteração direta dos seus campos.

Característica Tuple ValueTuple
Categoria Classe (referência) Struct (valor)
Membros Propriedades somente leitura Campos públicos mutáveis
Nomes personalizados Não Sim
Desconstrução Sim, via métodos de extensão Sim, nativa
Árvores de expressão Sim Não

Em geral, prefira ValueTuple no código novo. A Tuple ainda aparece em APIs antigas e em cenários com árvores de expressão, onde a sintaxe moderna não é suportada.

Quando evitar tuplas no C#

As tuplas no C# são ótimas para agrupamentos curtos e temporários. Porém, elas não substituem tipos nomeados em todos os casos.

Em APIs públicas, a documentação recomenda considerar uma classe, uma struct ou um record. Isso acontece porque os nomes dos itens não existem em tempo de execução.

Na serialização para JSON, por exemplo, os nomes escolhidos se perdem. Além disso, o System.Text.Json ignora campos por padrão, e os itens de um ValueTuple são campos.

// Melhor para contratos públicos
public record FullName(string FirstName, string LastName);

Outra alternativa leve são os tipos anônimos, que funcionam bem em projeções LINQ. Para comparar as duas opções, leia o artigo sobre tipos anônimos no C#.

Por fim, desde o C# 12 você pode criar um alias para um tipo de tupla com using. Isso melhora a leitura, mas não cria um novo tipo, então um record continua sendo a opção mais segura.

using Range = (int Min, int Max);

Conclusão

As tuplas no C# oferecem uma flexibilidade enorme, permitindo agrupar valores de tipos diferentes em uma única variável. Elas são tipos de referência no caso da classe Tuple e tipos de valor no caso da ValueTuple.

Na maior parte do tempo, a sintaxe com parênteses é o melhor caminho. Ela é curta, permite nomear os itens e suporta desconstrução, comparação e cópia com with.

Quando o dado crescer ou ganhar importância no sistema, troque a tupla por um record e mantenha o código fácil de entender.

Para continuar estudando a linguagem, confira também o artigo completo sobre C#.

FAQ

Qual a diferença entre Tuple e ValueTuple?

Tuple é uma classe imutável, com propriedades somente leitura. ValueTuple é uma struct mutável, com campos públicos, e é a base da sintaxe com parênteses.

Posso nomear os itens de uma tupla?

Sim, desde que você use a sintaxe com parênteses, como (string Name, int Age). Os nomes Item1, Item2 e seguintes continuam disponíveis.

Quantos itens uma tupla pode ter?

Com a sintaxe moderna, não há um limite prático, porque o compilador aninha tuplas automaticamente. Na classe Tuple, o oitavo parâmetro é o TRest, que guarda os itens restantes.

Tuplas são boas para retornar vários valores?

Sim, esse é um dos usos mais comuns. Elas substituem parâmetros out e evitam criar classes só para o retorno.

Devo usar tuplas em APIs públicas?

Em geral, não. Para contratos públicos, prefira records, classes ou structs, que preservam os nomes em tempo de execução.

Glossário

Termo Significado
Tupla Estrutura que agrupa vários valores, de tipos iguais ou diferentes, em uma única variável
Tuple Classe do .NET que representa uma tupla imutável e de referência
ValueTuple Struct do .NET que representa uma tupla mutável e de valor
Tipo de referência Tipo cujas variáveis guardam uma referência para o objeto no heap
Tipo de valor Tipo cujas variáveis guardam o próprio valor
Desconstrução Extração dos itens de uma tupla ou objeto para variáveis separadas
Descarte Uso do _ para ignorar um valor que não interessa
TRest Parâmetro de tipo que guarda os itens excedentes de uma Tuple com oito posições

Fontes