Pular para o conteúdo

Tuplas em Rust

Uma tupla junta vários valores, que podem ser de tipos diferentes, em um só. É um jeito rápido de agrupar coisas relacionadas sem precisar criar um tipo novo só para elas.

fn main() {
let person: (&str, i32, bool) = ("Alice", 30, true);
println!("{:?}", person);
}

Saída:

("Alice", 30, true)
▶ Experimente você mesmo

Repare que a tupla mistura um &str, um i32 e um bool em um único valor. Um array não consegue fazer isso, porque todos os elementos de um array precisam ser do mesmo tipo.

Para imprimir uma tupla inteira, use {:?} (formatação de debug) em vez de {}. O {} simples não funciona com tuplas.

Use .0, .1, .2 e assim por diante. A contagem das posições começa em zero:

fn main() {
let person = ("Alice", 30, true);
println!("Nome: {}", person.0);
println!("Idade: {}", person.1);
println!("Ativa: {}", person.2);
}

Saída:

Nome: Alice
Idade: 30
Ativa: true
▶ Experimente você mesmo

A posição precisa ser um número escrito diretamente no código. Você não pode usar uma variável, como person.i, porque cada posição pode guardar um tipo diferente, e o Rust precisa saber o tipo na hora de compilar.

Dá para tirar cada valor e colocar em uma variável própria, tudo em uma linha:

fn main() {
let person = ("Alice", 30, true);
let (name, age, is_active) = person;
println!("{name} tem {age} anos, ativa: {is_active}");
}

Saída:

Alice tem 30 anos, ativa: true
▶ Experimente você mesmo

Isso costuma ficar mais claro do que repetir .0, .1, .2, principalmente quando a tupla tem mais de dois ou três campos.

Uma função só pode declarar um tipo de retorno. Por isso, a tupla é o jeito natural de devolver mais de um valor:

fn min_max(numbers: &[i32]) -> (i32, i32) {
let min = *numbers.iter().min().unwrap();
let max = *numbers.iter().max().unwrap();
(min, max)
}
fn main() {
let (smallest, largest) = min_max(&[4, 8, 1, 9, 3]);
println!("mínimo: {smallest}, máximo: {largest}");
}

Saída:

mínimo: 1, máximo: 9
▶ Experimente você mesmo

Não se preocupe ainda com &[i32], .iter() ou .unwrap(). Eles são explicados nas páginas de Slices, Iteradores e Option. O que importa aqui é o tipo de retorno (i32, i32) e como o main o desestrutura em duas variáveis logo de cara.

Se uma tupla for declarada com mut, você pode alterar os campos um de cada vez, usando as mesmas posições .0, .1. Os valores podem mudar, mas o formato não: uma tupla sempre mantém a quantidade e os tipos de campos com que começou.

A desestruturação também funciona no sentido contrário: você pode atribuir a variáveis que já existem. Isso dá uma troca de valores elegante em uma linha, sem variável temporária. Se você só quer alguns dos campos, escreva _ nos que não precisa:

fn main() {
let mut point = (3, 4);
point.0 += 10; // altera um campo no lugar
println!("o ponto é {:?}", point);
let mut a = 1;
let mut b = 2;
(a, b) = (b, a); // troca os dois valores
println!("a = {a}, b = {b}");
let (x, _) = point; // fica com x, ignora o segundo campo
println!("x = {x}");
}

Saída:

o ponto é (13, 4)
a = 2, b = 1
x = 13
▶ Experimente você mesmo

A troca funciona porque o Rust monta primeiro o lado direito, (b, a). Só depois ele desempacota esse valor em a e b. Dá para fazer muito mais com _ e padrões na página de Pattern matching.

Na verdade, você já encontrou uma tupla sem perceber. () se chama tipo unit (unit type). É o que uma função retorna quando não tem nada de útil para devolver. É o tipo de retorno implícito de fn main().

Tuplas são ótimas para agrupamentos rápidos e sem nome, principalmente para retornar dois ou três valores de uma função. Quando um grupo de valores representa um conceito de verdade, com campos que você vai chamar pelo nome ao longo do código (como uma Person com nome e idade), uma struct costuma ser a escolha melhor e mais legível.

Escreva uma função sum_and_product(a: i32, b: i32) -> (i32, i32) que devolve a soma e o produto de dois números. No main, chame-a com 5 e 6 e desestruture o resultado. Dica: a última linha da função é a própria tupla, (a + b, a * b), sem ponto e vírgula.

Ver solução
fn sum_and_product(a: i32, b: i32) -> (i32, i32) {
(a + b, a * b)
}
fn main() {
let (sum, product) = sum_and_product(5, 6);
println!("soma = {sum}, produto = {product}");
}

Saída:

soma = 11, produto = 30
▶ Experimente você mesmo

Um retângulo pode ser guardado como uma tupla de dois pontos, em que cada ponto também é uma tupla: let rect = ((1, 2), (5, 5));. Calcule a largura, a altura e a área. Dica: tuplas podem ser aninhadas, e rect.1.0 quer dizer “o primeiro campo do segundo ponto”.

Ver solução
fn main() {
let rect = ((1, 2), (5, 5));
let width = rect.1.0 - rect.0.0;
let height = rect.1.1 - rect.0.1;
println!("largura {width}, altura {height}, área {}", width * height);
}

Saída:

largura 4, altura 3, área 12
▶ Experimente você mesmo

Você também pode desestruturar os dois pontos de uma vez: let ((x1, y1), (x2, y2)) = rect;. Dá mais trabalho para digitar, mas x2 - x1 é mais fácil de ler do que rect.1.0 - rect.0.0. Quando os pontos começam a se acumular assim, é sinal de que uma struct com campos nomeados ficaria mais clara.

  • Uma tupla agrupa valores que podem ser de tipos diferentes: (name, age, is_active).
  • Acesse os campos com .0, .1, .2 ou tire todos de uma vez com desestruturação.
  • Tuplas são um jeito natural de retornar vários valores de uma função.
  • Os campos de uma tupla mut podem mudar, mas o tamanho e os tipos não. (a, b) = (b, a); troca dois valores.
  • Para agrupamentos com nome e que você vai reutilizar, prefira uma struct.

Teste o que aprendeu

1. O que uma tupla consegue guardar e um array não?

2. Como você acessa o segundo elemento de uma tupla chamada person?

3. Depois de let mut a = 1; let mut b = 2; (a, b) = (b, a);, quanto valem a e b?

Pontuação: 0 / 3