Uma tupla em Rust é um tipo composto de tamanho fixo que pode conter valores de tipos diferentes. Tuplas são escritas como (T1, T2, T3, ...) e são extremamente úteis para agrupar valores relacionados sem criar uma struct, retornar múltiplos valores de funções, e fazer desestruturação (pattern matching). O tipo unitário () (tupla vazia) é um caso especial fundamental na linguagem.
Quando Usar Tuplas
Use tuplas quando:
- Precisa agrupar temporariamente valores de tipos diferentes.
- Quer retornar múltiplos valores de uma função.
- Precisa de desestruturação rápida em
let,matchou parâmetros de closure. - O significado dos campos é óbvio pelo contexto (ex.: coordenada
(x, y)).
Se os campos precisam de nomes descritivos ou a tupla é usada em muitos lugares, prefira criar uma struct.
Criando Tuplas
fn main() {
// Tupla com tipos variados
let pessoa: (&str, u32, f64) = ("Ana", 28, 1.65);
// Tipo inferido
let coordenada = (3.5, -2.1);
// Tupla de um único elemento (note a vírgula!)
let singleton = (42,);
// Sem vírgula, (42) é apenas um inteiro entre parênteses
// Tupla vazia — o tipo unitário ()
let vazio: () = ();
// Tupla aninhada
let dados = ("Rust", (2015, 5, 15), true);
// Tupla com elementos mutáveis
let mut ponto = (0, 0);
ponto.0 = 10;
ponto.1 = 20;
println!("Pessoa: {:?}", pessoa);
println!("Coordenada: {:?}", coordenada);
println!("Singleton: {:?}", singleton);
println!("Ponto: {:?}", ponto);
println!("Dados: {:?}", dados);
}
Acessando Elementos e Desestruturação
fn main() {
let rgb = (255u8, 128u8, 0u8);
// Acesso por índice (começa em 0)
println!("R: {}", rgb.0);
println!("G: {}", rgb.1);
println!("B: {}", rgb.2);
// Desestruturação com let
let (r, g, b) = rgb;
println!("Cor: R={}, G={}, B={}", r, g, b);
// Ignorar campos com _
let (vermelho, _, _) = rgb;
println!("Só o vermelho: {}", vermelho);
// Desestruturação parcial com ..
let tupla = (1, 2, 3, 4, 5);
let (primeiro, ..) = tupla;
let (.., ultimo) = tupla;
let (primeiro2, .., ultimo2) = tupla;
println!("Primeiro: {}, Último: {}", primeiro, ultimo);
println!("Primeiro: {}, Último: {}", primeiro2, ultimo2);
// Desestruturação em match
let resultado = (true, 42);
match resultado {
(true, valor) => println!("Sucesso: {}", valor),
(false, codigo) => println!("Erro: código {}", codigo),
}
}
Tabela de Características
| Característica | Descrição |
|---|---|
| Tamanho | Fixo em tempo de compilação |
| Tipos dos elementos | Podem ser diferentes |
| Máximo de elementos | 12 para a maioria dos traits (Debug, Clone, etc.) |
| Acesso | Por índice (tupla.0, tupla.1) ou desestruturação |
| Mutabilidade | Elementos mutáveis se a variável for mut |
Debug | Implementado para tuplas de até 12 elementos |
PartialEq / Eq | Implementado se todos os tipos implementam |
PartialOrd / Ord | Implementado se todos os tipos implementam |
Clone / Copy | Implementado se todos os tipos implementam |
Default | Implementado se todos os tipos implementam |
Hash | Implementado se todos os tipos implementam |
| Stack allocation | Sim (quando usado como variável local) |
Exemplos Práticos
1. Retornando Múltiplos Valores de Funções
fn dividir(dividendo: f64, divisor: f64) -> (f64, f64) {
let quociente = (dividendo / divisor).floor();
let resto = dividendo % divisor;
(quociente, resto)
}
fn min_max(valores: &[i32]) -> Option<(i32, i32)> {
if valores.is_empty() {
return None;
}
let mut min = valores[0];
let mut max = valores[0];
for &v in &valores[1..] {
if v < min { min = v; }
if v > max { max = v; }
}
Some((min, max))
}
fn analisar_texto(texto: &str) -> (usize, usize, usize) {
let caracteres = texto.len();
let palavras = texto.split_whitespace().count();
let linhas = texto.lines().count();
(caracteres, palavras, linhas)
}
fn main() {
let (quociente, resto) = dividir(17.0, 5.0);
println!("17 / 5 = {} resto {}", quociente, resto);
let dados = [38, 27, 43, 3, 9, 82, 10];
if let Some((min, max)) = min_max(&dados) {
println!("Min: {}, Max: {}", min, max);
}
let texto = "Rust é uma linguagem\nde programação\nsegura e rápida";
let (chars, palavras, linhas) = analisar_texto(texto);
println!("Caracteres: {}, Palavras: {}, Linhas: {}", chars, palavras, linhas);
}
2. Tuplas em Iteradores e Closures
fn main() {
let nomes = vec!["Ana", "Bruno", "Carla"];
let idades = vec![28, 35, 22];
// zip cria tuplas
let pessoas: Vec<(&str, i32)> = nomes.iter()
.copied()
.zip(idades.iter().copied())
.collect();
println!("Pessoas: {:?}", pessoas);
// enumerate retorna tuplas (índice, valor)
for (i, nome) in nomes.iter().enumerate() {
println!("{}. {}", i + 1, nome);
}
// Desestruturação em closures
let mais_velha = pessoas.iter()
.max_by_key(|&(_, idade)| idade)
.unwrap();
println!("Mais velha: {} ({})", mais_velha.0, mais_velha.1);
// Ordenar por segundo elemento da tupla
let mut dados = vec![("C", 3), ("A", 1), ("B", 2)];
dados.sort_by_key(|&(_, v)| v);
println!("Ordenado: {:?}", dados); // [("A", 1), ("B", 2), ("C", 3)]
}
3. O Tipo Unitário () e Funções Sem Retorno
use std::collections::HashSet;
// Funções sem retorno explícito retornam ()
fn saudar(nome: &str) {
println!("Olá, {}!", nome);
}
// Equivalente explícito
fn saudar_explicito(nome: &str) -> () {
println!("Olá, {}!", nome);
}
fn main() {
// () é um tipo real com exatamente um valor: ()
let resultado: () = saudar("Mundo");
println!("Tipo de resultado: {:?}", resultado); // ()
// () em genéricos: HashMap<K, ()> é basicamente um HashSet
let mut conjunto: HashSet<&str> = HashSet::new();
conjunto.insert("item");
// () como "nenhum dado" em enums
enum Evento {
Clique(i32, i32), // com dados
Tecla(char), // com dados
Fechar, // sem dados (implicitamente ())
}
// Result<(), Error> — operação que pode falhar mas não retorna valor
fn salvar_arquivo(conteudo: &str) -> Result<(), String> {
if conteudo.is_empty() {
Err("Conteúdo vazio".to_string())
} else {
println!("Salvando: {}...", &conteudo[..20.min(conteudo.len())]);
Ok(())
}
}
match salvar_arquivo("dados importantes") {
Ok(()) => println!("Salvo com sucesso!"),
Err(e) => println!("Erro: {}", e),
}
}
4. Tuplas vs Structs — Quando Usar Cada Um
// Tupla: bom para valores temporários e óbvios
fn coordenadas_mouse() -> (i32, i32) {
(100, 200) // x, y — óbvio pelo contexto
}
// Struct: melhor quando os campos precisam de nomes
#[derive(Debug)]
struct Retangulo {
largura: f64,
altura: f64,
}
// Tuple struct: meio-termo — tipo nomeado com campos posicionais
#[derive(Debug)]
struct Cor(u8, u8, u8);
#[derive(Debug)]
struct Metros(f64);
#[derive(Debug)]
struct Quilometros(f64);
fn main() {
let (x, y) = coordenadas_mouse();
println!("Mouse em ({}, {})", x, y);
let ret = Retangulo { largura: 10.0, altura: 5.0 };
println!("Retângulo: {:?}", ret);
// Tuple structs dão type safety
let vermelho = Cor(255, 0, 0);
println!("Vermelho: {:?}", vermelho);
let distancia = Metros(1500.0);
let outra = Quilometros(1.5);
// Metros e Quilometros são tipos diferentes!
// Não podemos comparar ou misturá-los acidentalmente
println!("{:?} e {:?}", distancia, outra);
}
5. Padrões Avançados com Tuplas
fn main() {
// Swap de variáveis com tuplas
let mut a = 10;
let mut b = 20;
(a, b) = (b, a); // swap!
println!("a={}, b={}", a, b); // a=20, b=10
// Pattern matching complexo
let resultados: Vec<(bool, i32)> = vec![
(true, 100),
(false, -1),
(true, 200),
(false, -2),
(true, 150),
];
let (sucessos, falhas): (Vec<_>, Vec<_>) = resultados
.iter()
.partition(|&&(ok, _)| ok);
println!("Sucessos: {:?}", sucessos);
println!("Falhas: {:?}", falhas);
// Tuplas como chaves de HashMap
use std::collections::HashMap;
let mut grid: HashMap<(i32, i32), &str> = HashMap::new();
grid.insert((0, 0), "origem");
grid.insert((1, 0), "leste");
grid.insert((0, 1), "norte");
for ((x, y), nome) in &grid {
println!("({}, {}) = {}", x, y, nome);
}
// Comparação lexicográfica de tuplas
// Compara primeiro elemento, depois segundo, etc.
assert!((1, 2) < (1, 3));
assert!((1, 2) < (2, 0));
assert!((1, 2, 3) == (1, 2, 3));
println!("Comparações de tuplas OK!");
}
Características de Desempenho
As tuplas têm zero overhead em tempo de execução. O compilador sabe exatamente o layout de memória e gera código otimizado.
| Aspecto | Tupla | Struct | Vec (de tuplas) |
|---|---|---|---|
| Alocação | Stack | Stack | Heap |
| Overhead | 0 bytes | 0 bytes | 24 bytes |
| Acesso a campo | O(1) | O(1) | O(1) |
| Layout | Compilador decide | Compilador decide | Contíguo |
| Tamanho | sum(size_of campos) + padding | Igual | Dinâmico |
Padding: O compilador pode inserir bytes de padding entre campos de uma tupla para alinhar os dados na memória. O layout exato não é garantido (a menos que use #[repr(C)] em uma struct equivalente).
Comparação: Tuplas implementam comparação lexicográfica — compara o primeiro elemento, depois o segundo em caso de empate, e assim por diante. Isso é útil para ordenação multi-critério:
fn main() {
let mut alunos = vec![
("Ana", 9.5),
("Bruno", 8.0),
("Carla", 9.5),
("Daniel", 7.0),
];
// Ordena por nota (decrescente), depois por nome (crescente)
alunos.sort_by(|a, b| {
b.1.partial_cmp(&a.1)
.unwrap()
.then(a.0.cmp(&b.0))
});
for (nome, nota) in &alunos {
println!("{}: {}", nome, nota);
}
// Ana: 9.5
// Carla: 9.5
// Bruno: 8.0
// Daniel: 7.0
}
Limitações das Tuplas
- Máximo de 12 elementos para traits derivados automaticamente (
Debug,Clone, etc.). Tuplas maiores podem existir, mas não terão esses traits. - Sem nomes de campos — o significado de
tupla.3pode não ser óbvio. Use structs para dados complexos. - Sem iteração genérica — não existe
.iter()para tuplas (os elementos podem ter tipos diferentes). - Tipos rígidos —
(i32, String)e(String, i32)são tipos completamente diferentes.
Veja Também
- Arrays em Rust — coleção de tamanho fixo com tipos homogêneos
- Structs, Enums e Pattern Matching — quando usar structs em vez de tuplas
- Variáveis, Tipos e Funções — fundamentos dos tipos em Rust
- HashMap em Rust — usando tuplas como chaves de mapas
- Ranges em Rust — faixas para iteração
- Tipos Numéricos — os tipos que frequentemente compõem tuplas