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 aoTuple.Create. - Nomeie os itens em retornos e parâmetros, para não depender de
Item1eItem2. - 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.