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
- The Rust Programming Language - Capítulo 3.2: Data Types — Seção oficial sobre tipos de dados, incluindo tuplas e arrays
- Rust by Example: Tuples — Exemplos práticos de criação, acesso e destruturação de tuplas
- Rust by Example: Arrays — Tutorial interativo sobre arrays, iteração e slicing
- Rust Reference: Array types — Documentação técnica detalhada sobre tipos array em Rust
- Rust Reference: Tuple types — Especificação formal dos tipos tupla na linguagem Rust
- Rust Performance Book - Stack vs Heap — Guia de performance explicando alocação na stack (arrays/tuplas) vs heap (Vec)