C# Tuplas

Um método em C# devolve um valor com return. Mas e se a Ana quiser saber a menor e a maior nota de uma turma de uma vez? Criar uma classe ou um record só para isso parece exagero. Para esses agrupamentos rápidos, o C# tem as tuplas: alguns valores entre parênteses, ("Bia", 9), que andam juntos sem precisar de um tipo com nome.

Você vai aprender
  • O que é uma tupla e como criar uma: (string, int) aluno = ("Bia", 9);.
  • Os nomes Item1, Item2… e como dar nomes melhores aos elementos.
  • Como um método devolve vários valores com uma tupla.
  • Desconstrução (var (menor, maior) = …), o descarte _ e a troca de variáveis.
  • Igualdade (==), listas de tuplas e switch com tuplas.
  • Os erros CS0029 e CS1061 e o aviso CS8123.
  • Quando usar tupla e quando usar record ou classe.

Todos os programas desta aula rodaram com dotnet run arquivo.cs no .NET 10 da máquina da Ana, na pasta ~/cs.


A ideia: um par de meias

Algumas coisas andam em grupo sem ganhar um nome próprio: um par de meias, as coordenadas de um ponto no mapa, "nome e telefone" anotados num guardanapo. Você não cria uma ficha de cadastro para isso; só junta as coisas. A tupla é esse guardanapo: um grupinho de valores, de tipos que podem ser diferentes, numa ordem fixa.

O nome vem da matemática: par, trio, quádrupla, quíntupla... uma "ênupla" ou tupla é um grupo de qualquer tamanho.


A primeira tupla

(string, int) aluno = ("Bia", 9);
Console.WriteLine(aluno);
Console.WriteLine(aluno.Item1);
Console.WriteLine(aluno.Item2);
(string, int)
O tipo da tupla: os tipos dos elementos, na ordem, entre parênteses. Esta tupla tem um texto e um número.
("Bia", 9)
O valor: os elementos entre parênteses, separados por vírgula.
aluno.Item1, aluno.Item2
Os elementos, pela posição: Item1 é o primeiro, Item2 o segundo, e assim por diante. (Começa do 1, não do 0, ao contrário dos arrays.)
Console.WriteLine(aluno)
Uma tupla aparece como os valores entre parênteses.
$ dotnet run basico.cs
(Bia, 9)
Bia
9

Dar nomes aos elementos

Item1 e Item2 não dizem nada a quem lê o código. Dê nomes:

(string Nome, int Idade) ana = ("Ana", 29);
Console.WriteLine($"{ana.Nome} tem {ana.Idade} anos");

var bia = (Nome: "Bia", Idade: 31);
Console.WriteLine(bia.Nome);

string cidade = "Recife";
int habitantes = 1_488_920;
var lugar = (cidade, habitantes);
Console.WriteLine($"{lugar.cidade}: {lugar.habitantes:N0}");

Console.WriteLine(ana.Item1);
(string Nome, int Idade) ana
Os nomes vão no tipo, depois de cada tipo. Os nomes dos elementos seguem o PascalCase, como propriedades.
var bia = (Nome: "Bia", Idade: 31);
Ou os nomes vão no valor, com Nome:, e o var descobre o tipo (veja var).
var lugar = (cidade, habitantes);
Feita a partir de variáveis, a tupla pega os nomes delas sozinha: lugar.cidade, lugar.habitantes.
ana.Item1
Os nomes Item1, Item2 continuam valendo, mesmo com nomes dados.
$ dotnet run nomes.cs
Ana tem 29 anos
Bia
Recife: 1.488.920
Ana

Missão: dê os nomes Nome e Nota aos elementos da tupla e troque aluno.Item1 e aluno.Item2 por aluno.Nome e aluno.Nota. A saída continua Bia tirou 9, mas o código fica bem mais claro.

(string, int) aluno = ("Bia", 9);
Console.WriteLine($"{aluno.Item1} tirou {aluno.Item2}");
(string Nome, int Nota) aluno = ("Bia", 9);
Console.WriteLine($"{aluno.Nome} tirou {aluno.Nota}");

Devolver vários valores de um método

É o uso mais comum das tuplas. O tipo de retorno do método é uma tupla, e o return devolve os valores entre parênteses:

int[] notas = [7, 10, 4, 8];
(int Menor, int Maior) resultado = Extremos(notas);
Console.WriteLine($"Menor: {resultado.Menor}, maior: {resultado.Maior}");

var (menor, maior) = Extremos(notas);
Console.WriteLine($"Diferença: {maior - menor}");

(int Menor, int Maior) Extremos(int[] numeros)
{
    int menor = numeros[0];
    int maior = numeros[0];
    foreach (int n in numeros)
    {
        if (n < menor) menor = n;
        if (n > maior) maior = n;
    }
    return (menor, maior);
}
(int Menor, int Maior) Extremos(int[] numeros)
O método devolve uma tupla de dois int, chamados Menor e Maior. Os nomes servem de documentação para quem usa o método.
return (menor, maior);
Devolve os dois valores juntos, na ordem do tipo de retorno.
resultado.Menor
Quem chamou guarda a tupla e usa os nomes.
var (menor, maior) = Extremos(notas);
A desconstrução: abre a tupla em duas variáveis separadas, de uma vez. As variáveis podem ter qualquer nome; o que vale é a ordem.
$ dotnet run extremos.cs
Menor: 4, maior: 10
Diferença: 6

Antes das tuplas (C# 7, 2017), o jeito de devolver vários valores era o out (veja ref, out e in), como no int.TryParse(texto, out int n). Para código novo, uma tupla costuma ser mais clara.


Desconstrução: mais truques

var (_, maior) = Extremos([7, 10, 4, 8]);
Console.WriteLine(maior);

int a = 1, b = 2;
(a, b) = (b, a);
Console.WriteLine($"a = {a}, b = {b}");

(int Menor, int Maior) Extremos(int[] numeros) => (numeros.Min(), numeros.Max());
var (_, maior)
O _ é o descarte: "não quero este valor". Só o maior vira variável.
(a, b) = (b, a);
Troca os valores de duas variáveis numa linha só. Sem tupla, seria preciso uma terceira variável auxiliar.
=> (numeros.Min(), numeros.Max())
A mesma função, em uma linha, com o Min e o Max do LINQ (veja Agregar).
$ dotnet run truques.cs
10
a = 2, b = 1

Tuplas são valores

Uma tupla é um tipo de valor (um struct chamado ValueTuple; veja Structs). Isso tem três consequências:

var p1 = (Nome: "Ana", Idade: 29);
var p2 = (Nome: "Ana", Idade: 29);
var p3 = ("Ana", 30);
Console.WriteLine(p1 == p2);
Console.WriteLine(p1 == p3);
Console.WriteLine(p1.Equals(p2));

var copia = p1;
copia.Idade = 30;
Console.WriteLine($"{p1} {copia}");
$ dotnet run igual.cs
True               « mesmo conteúdo
False              « 29 × 30
True
(Ana, 29) (Ana, 30)   « a cópia é independente
  1. == compara elemento por elemento, na ordem. Os nomes não importam: p1 == p3 compara mesmo sem p3 ter nomes.
  2. copia = p1 copia tudo: mudar a cópia não muda o original.
  3. Os elementos podem ser mudados: copia.Idade = 30 funciona. Ao contrário dos records, tuplas não são imutáveis.

Por baixo, o tipo de verdade é System.ValueTuple`2[System.String,System.Int32] (o `2 quer dizer "dois elementos"). Os nomes Nome e Idade só existem para o compilador; o programa rodando só conhece Item1 e Item2. Por isso, uma tupla com outros nomes, mas os mesmos tipos, pode receber outra: (string A, int B) outro = p1; funciona.


Listas de tuplas

Uma List de tuplas é um jeito rápido de montar uma "tabelinha":

List<(string Produto, decimal Preco)> compras =
[
    ("Café", 18.90m),
    ("Pão", 9.50m),
    ("Leite", 6.20m)
];

decimal total = 0;
foreach (var (produto, preco) in compras)
{
    Console.WriteLine($"{produto,-6} {preco,8:C}");
    total += preco;
}
Console.WriteLine($"{"Total",-6} {total,8:C}");
List<(string Produto, decimal Preco)>
Uma lista em que cada item é uma tupla de texto e preço (veja List<T>).
foreach (var (produto, preco) in compras)
O foreach também desconstrói: cada volta já entrega o produto e o preço em variáveis separadas.
{produto,-6} {preco,8:C}
Alinhamento: o produto ocupa 6 casas à esquerda e o preço 8 casas à direita (veja Formatar Números).
$ dotnet run compras.cs
Café   R$ 18,90
Pão     R$ 9,50
Leite   R$ 6,20
Total  R$ 34,60

Tuplas no switch

Uma tupla deixa o switch olhar para dois valores ao mesmo tempo. Pedra, papel e tesoura, entre a Ana e a Bia:

Console.WriteLine(Vencedor("pedra", "tesoura"));
Console.WriteLine(Vencedor("papel", "tesoura"));
Console.WriteLine(Vencedor("papel", "papel"));

string Vencedor(string ana, string bia) => (ana, bia) switch
{
    var (x, y) when x == y => "empate",
    ("pedra", "tesoura") or ("tesoura", "papel") or ("papel", "pedra") => "Ana venceu",
    _ => "Bia venceu",
};
$ dotnet run jokenpo.cs
Ana venceu
Bia venceu
empate

O (ana, bia) switch monta uma tupla e compara com cada padrão: ("pedra", "tesoura") casa quando o primeiro é "pedra" e o segundo é "tesoura". A aula Pattern Matching explica o when e o or.


Tupla, record ou classe?

TuplaRecordClasse
Precisa declarar um tipo?NãoSim, uma linhaSim
Nome do tipoNenhum: (string, int)ProdutoProduto
FamíliaValorReferência (ou valor, com record struct)Referência
Muda depois de criado?SimNãoVocê decide
== compara o conteúdoSimSimNão
Métodos própriosNãoSimSim
Bom paraResultados de um método, grupinhos locaisDados que circulam pelo programaObjetos com comportamento e estado

Regra prática: use tupla quando o grupo é pequeno (2 ou 3 valores) e vive pouco (sai de um método e é desmontado logo em seguida). Se a mesma tupla começa a aparecer em vários lugares do programa, ou se você sente falta de um método nela, é hora de criar um record.

As tuplas antigas: Tuple<T1, T2>

Em código antigo (de antes de 2017), você pode encontrar Tuple.Create("Bia", 9) e o tipo Tuple<string, int>. São outra coisa: uma classe (tipo de referência), imutável, e só com Item1, Item2, sem nomes. Em código novo, use as tuplas com parênteses desta aula.


Quando dá errado

CS0029: tupla com o número errado de elementos
(string, int) t = ("Ana", 29, true);
$ dotnet run erros.cs
/home/ana/cs/erros.cs(1,19): error CS0029: Não é possível converter implicitamente tipo "(string, int, bool)" em "(string, int)"

O que aconteceu: o valor tem três elementos e o tipo, dois. Com os tipos fora de ordem, como em (int, string) t = ("Ana", 29);, o mesmo CS0029 aparece para cada elemento: Não é possível converter implicitamente tipo "string" em "int".

Como resolver: o valor e o tipo precisam ter os mesmos elementos, na mesma ordem.

CS1061: "… não contém uma definição para …"
var t = (Nome: "Ana", Idade: 29);
Console.WriteLine(t.Item3);
Console.WriteLine(t.Cidade);
$ dotnet run erros2.cs
/home/ana/cs/erros2.cs(2,21): error CS1061: ‘(string Nome, int Idade)’ não contém uma definição para "Item3" e não foi possível encontrar nenhum método de extensão "Item3" que aceite um primeiro argumento do tipo ‘(string Nome, int Idade)’ (você está se esquecendo de usar uma diretiva ou uma referência de assembly?)
/home/ana/cs/erros2.cs(3,21): error CS1061: ‘(string Nome, int Idade)’ não contém uma definição para "Cidade" e não foi possível encontrar nenhum método de extensão "Cidade" que aceite um primeiro argumento do tipo ‘(string Nome, int Idade)’ (você está se esquecendo de usar uma diretiva ou uma referência de assembly?)

O que aconteceu: a tupla tem dois elementos (não há Item3) e nenhum se chama Cidade. A mensagem mostra o tipo da tupla com os nomes, o que ajuda a ver o que existe.

Como resolver: use um dos nomes do tipo (Nome, Idade, Item1, Item2).

CS8123: "O nome do elemento de tupla … foi ignorado …"
(string Nome, int Idade) p = (N: "Ana", I: 29);
Console.WriteLine(p.Nome);
$ dotnet run aviso.cs
/home/ana/cs/aviso.cs(1,31): warning CS8123: O nome do elemento de tupla 'N' foi ignorado porque um nome diferente ou nenhum nome foi especificado pelo tipo de destino '(string Nome, int Idade)'.
/home/ana/cs/aviso.cs(1,41): warning CS8123: O nome do elemento de tupla 'I' foi ignorado porque um nome diferente ou nenhum nome foi especificado pelo tipo de destino '(string Nome, int Idade)'.
Ana

O que aconteceu: os nomes do valor (N, I) são diferentes dos do tipo (Nome, Idade). Vale a ordem, e os nomes do tipo ganham. O compilador avisa porque isso costuma ser um engano.

Como resolver: tire os nomes do valor (("Ana", 29)) ou use os mesmos do tipo.


Referência

EscritaSignificado
(string, int) t = ("Bia", 9);Tupla sem nomes: t.Item1, t.Item2
(string Nome, int Nota) t = ("Bia", 9);Tupla com nomes no tipo
var t = (Nome: "Bia", Nota: 9);Tupla com nomes no valor
(int Menor, int Maior) Metodo() { return (1, 9); }Método que devolve vários valores
var (menor, maior) = Metodo();Desconstrução
var (_, maior) = Metodo();Descarte de um elemento
(a, b) = (b, a);Troca duas variáveis
t1 == t2Compara elemento por elemento
List<(string, decimal)>, foreach (var (a, b) in lista)Listas de tuplas
(x, y) switch { (1, 2) => …, _ => … }switch com dois valores

Exercícios

1. O tipo

Complete o tipo da tupla ("Café", 18.90m), sem nomes:

item = ("Café", 18.90m);

2. O segundo elemento

Na tupla sem nomes var t = ("Ana", 29);, como se chama o elemento que guarda o 29?

t.

3. Desconstruir

Complete para abrir a tupla devolvida por Extremos(notas) nas variáveis menor e maior:

= Extremos(notas);

4. O que faz (a, b) = (b, a);?

5. ("Ana", 29) == (Nome: "Ana", Idade: 29) dá:

6. Um mesmo grupo "nome, e-mail, telefone, endereço" aparece em dez métodos do programa. O que usar?

Missão final: a professora Ana quer saber quantos alunos passaram e quantos não passaram (média 6). O programa já chama Contar e desconstrói o resultado, mas o método devolve sempre (0, 0). Complete o método: percorra as notas com foreach, conte as notas >= media como aprovados e as outras como reprovados, e devolva as duas contagens com return (aprovados, reprovados);. A saída deve ser Aprovados: 3, reprovados: 2.

double[] notas = [8, 5.5, 9, 4, 7];
var (aprovados, reprovados) = Contar(notas, 6);
Console.WriteLine($"Aprovados: {aprovados}, reprovados: {reprovados}");

(int Aprovados, int Reprovados) Contar(double[] notas, double media)
{
    // conte aqui
    return (0, 0);
}
double[] notas = [8, 5.5, 9, 4, 7];
var (aprovados, reprovados) = Contar(notas, 6);
Console.WriteLine($"Aprovados: {aprovados}, reprovados: {reprovados}");

(int Aprovados, int Reprovados) Contar(double[] notas, double media)
{
    int aprovados = 0;
    int reprovados = 0;
    foreach (double n in notas)
    {
        if (n >= media)
        {
            aprovados++;
        }
        else
        {
            reprovados++;
        }
    }
    return (aprovados, reprovados);
}
$ dotnet run turma.cs      « a solução
Aprovados: 3, reprovados: 2

Resumo

  • Uma tupla junta alguns valores sem criar um tipo: (string, int) aluno = ("Bia", 9);. Os elementos são Item1, Item2…
  • Dê nomes: (string Nome, int Nota) ou (Nome: "Bia", Nota: 9). Os nomes só existem para o compilador (o aviso CS8123 aparece quando eles não batem).
  • O uso mais comum é devolver vários valores de um método: return (menor, maior);, e quem chama desconstrói com var (menor, maior) = …. O _ descarta; (a, b) = (b, a) troca.
  • Tuplas são tipos de valor (ValueTuple): copiadas na atribuição, comparáveis com == elemento por elemento, e mutáveis.
  • Use tupla para grupos pequenos e de vida curta; se o grupo se espalha pelo programa, crie um record. Próxima aula: Namespaces e using.