Tipos compostos: tuplas e arrays

1. Introdução aos tipos compostos em Rust

Em Rust, os tipos compostos permitem agrupar múltiplos valores em uma única entidade. Diferentemente dos tipos escalares (como i32, bool, char) que representam um único valor, os tipos compostos organizam coleções de dados de forma estruturada.

Os dois principais tipos compostos nativos em Rust são:

  • Tuplas: coleções heterogêneas (podem conter tipos diferentes)
  • Arrays: coleções homogêneas (todos os elementos do mesmo tipo)

Ambos têm tamanho fixo definido em tempo de compilação, o que os diferencia de estruturas dinâmicas como Vec (vetor). Essa característica traz benefícios de performance, pois os dados são alocados na stack.

2. Tuplas: agrupando valores de tipos diferentes

Tuplas permitem armazenar uma sequência de valores de tipos potencialmente diferentes. A sintaxe básica usa parênteses:

let tup: (i32, f64, char) = (500, 6.4, 'z');

O Rust infere os tipos automaticamente quando possível:

let tupla_simples = (10, "hello", 3.14); // (i32, &str, f64)

A destruturação (pattern matching) é uma forma elegante de extrair valores:

let tup = (500, 6.4, 'z');
let (x, y, z) = tup;
println!("x = {}, y = {}, z = {}", x, y, z); // x = 500, y = 6.4, z = z

3. Acessando elementos de uma tupla

Além da destruturação, podemos acessar elementos usando notação de ponto com índice:

let tup = (500, 6.4, 'z');
println!("Primeiro: {}, Segundo: {}, Terceiro: {}", tup.0, tup.1, tup.2);

Tuplas são extremamente úteis como valores de retorno de funções:

fn calcular_estatisticas(numeros: &[i32]) -> (i32, i32, f64) {
    let soma: i32 = numeros.iter().sum();
    let count = numeros.len() as i32;
    let media = soma as f64 / count as f64;
    (soma, count, media)
}

let resultado = calcular_estatisticas(&[10, 20, 30]);
println!("Soma: {}, Count: {}, Média: {:.2}", resultado.0, resultado.1, resultado.2);

Outro padrão comum é retornar um par (resultado, erro):

fn dividir(a: f64, b: f64) -> (Option<f64>, String) {
    if b == 0.0 {
        (None, "Divisão por zero!".to_string())
    } else {
        (Some(a / b), String::new())
    }
}

let (resultado, erro) = dividir(10.0, 2.0);
match resultado {
    Some(val) => println!("Resultado: {}", val),
    None => println!("Erro: {}", erro),
}

4. Limitações e particularidades das tuplas

Tuplas têm tamanho fixo — não é possível adicionar ou remover elementos após a criação. A mutabilidade pode ser aplicada à tupla inteira ou a elementos específicos:

let mut tup = (1, "abc", 3.5);
tup.0 = 42; // Permitido porque a tupla é mutável
// tup.1 = "xyz"; // Erro! O tipo do elemento 1 é &str, não pode mudar

let tup_imutavel = (1, 2, 3);
// tup_imutavel.0 = 10; // Erro! Tupla imutável

A tupla unitária () é um tipo especial que representa "nenhum valor". Funções que não retornam nada explicitamente retornam (). É útil como placeholder em genéricos ou como retorno de funções que produzem efeitos colaterais:

fn saudacao() -> () {
    println!("Olá, mundo!");
}

5. Arrays: coleções homogêneas de tamanho fixo

Arrays armazenam múltiplos valores do mesmo tipo com tamanho definido em tempo de compilação:

let arr: [i32; 3] = [1, 2, 3];

Uma sintaxe especial permite criar arrays com valor repetido:

let arr_repetido = [3; 5]; // Cria [3, 3, 3, 3, 3] - equivalente a [3, 3, 3, 3, 3]
let arr_zeros = [0; 100]; // Array de 100 zeros

É crucial entender a diferença entre arrays e vetores (Vec):

Característica Array Vec
Tamanho Fixo (compile-time) Dinâmico (runtime)
Alocação Stack Heap
Tipo [T; N] Vec<T>
Performance Mais rápido Mais flexível

6. Acessando e manipulando arrays

O acesso a elementos usa colchetes com índice baseado em zero:

let meses = ["Jan", "Fev", "Mar", "Abr", "Mai", "Jun"];
println!("Primeiro mês: {}", meses[0]); // Jan
println!("Último mês: {}", meses[5]);   // Jun

O Rust verifica limites em tempo de compilação quando possível. Para índices dinâmicos, a verificação ocorre em runtime:

let arr = [10, 20, 30];
let indice = 5;

// Isso compila, mas vai panic! em runtime
// println!("{}", arr[indice]);

// Tratamento seguro com get()
match arr.get(indice) {
    Some(valor) => println!("Valor: {}", valor),
    None => println!("Índice {} fora dos limites!", indice),
}

Podemos usar expect para simplificar o tratamento:

let valor = arr.get(1).expect("Índice inválido!");
println!("Valor: {}", valor); // 20

7. Iteração sobre arrays e tuplas

Arrays suportam iteração nativa com for:

let numeros = [10, 20, 30, 40, 50];

// Iteração por referência
for elemento in numeros.iter() {
    println!("Elemento: {}", elemento);
}

// Iteração por valor (consome o array)
for elemento in numeros {
    println!("Elemento por valor: {}", elemento);
}

Para obter índices junto com valores, use enumerate:

let letras = ['a', 'b', 'c', 'd'];
for (indice, valor) in letras.iter().enumerate() {
    println!("Índice {}: {}", indice, valor);
}

Tuplas não suportam iteração direta como arrays. Para iterar sobre uma tupla, precisamos usar bibliotecas externas ou acessar elemento por elemento:

let tup = (1, "dois", 3.0);
// Não é possível fazer: for item in tup { ... }
// Solução: acessar manualmente
println!("{} {} {}", tup.0, tup.1, tup.2);

8. Comparação e melhores práticas

Situação Recomendação
Dados heterogêneos (tipos diferentes) Tupla
Dados homogêneos, tamanho fixo conhecido Array
Dados homogêneos, tamanho variável Vec
Múltiplos campos nomeados Struct
Retorno múltiplo de função Tupla
Coleção grande de dados fixos Array (stack)

Exemplo completo: função utilitária que processa coordenadas usando tuplas e arrays:

type Ponto = (f64, f64);

fn calcular_distancia(p1: Ponto, p2: Ponto) -> f64 {
    let dx = p2.0 - p1.0;
    let dy = p2.1 - p1.1;
    (dx * dx + dy * dy).sqrt()
}

fn pontos_para_array(pontos: &[Ponto; 3]) -> [f64; 3] {
    let mut distancias = [0.0; 3];
    for (i, par) in pontos.windows(2).enumerate() {
        distancias[i] = calcular_distancia(par[0], par[1]);
    }
    // Última distância: do último ao primeiro
    distancias[2] = calcular_distancia(pontos[2], pontos[0]);
    distancias
}

fn main() {
    let vertices: [Ponto; 3] = [
        (0.0, 0.0),
        (3.0, 0.0),
        (1.5, 2.598),
    ];

    let distancias = pontos_para_array(&vertices);
    println!("Lados do triângulo: {:.2}, {:.2}, {:.2}",
             distancias[0], distancias[1], distancias[2]);
}

Performance: arrays e tuplas são armazenados na stack, garantindo acesso extremamente rápido. Vetores (Vec) alocam no heap e têm overhead de gerenciamento de memória. Para coleções pequenas e de tamanho fixo, arrays e tuplas são sempre preferíveis.

Referências