Tuplas em Rust: (T1, T2, ...)

Guia completo de tuplas em Rust: criação, desestruturação, unit type (), retorno de múltiplos valores, tuplas vs structs e exemplos práticos em português.

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, match ou 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ísticaDescrição
TamanhoFixo em tempo de compilação
Tipos dos elementosPodem ser diferentes
Máximo de elementos12 para a maioria dos traits (Debug, Clone, etc.)
AcessoPor índice (tupla.0, tupla.1) ou desestruturação
MutabilidadeElementos mutáveis se a variável for mut
DebugImplementado para tuplas de até 12 elementos
PartialEq / EqImplementado se todos os tipos implementam
PartialOrd / OrdImplementado se todos os tipos implementam
Clone / CopyImplementado se todos os tipos implementam
DefaultImplementado se todos os tipos implementam
HashImplementado se todos os tipos implementam
Stack allocationSim (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.

AspectoTuplaStructVec (de tuplas)
AlocaçãoStackStackHeap
Overhead0 bytes0 bytes24 bytes
Acesso a campoO(1)O(1)O(1)
LayoutCompilador decideCompilador decideContíguo
Tamanhosum(size_of campos) + paddingIgualDinâ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.3 pode 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