Em Rust, além dos tipos escalares, temos tipos compostos que agrupam múltiplos valores. Dois dos mais fundamentais são tuplas e arrays. Eles permitem armazenar coleções de dados, mas com diferenças importantes: tuplas podem conter tipos diferentes e têm tamanho fixo, enquanto arrays têm todos os elementos do mesmo tipo e tamanho fixo.

Dominar esses tipos é essencial para organizar dados de forma eficiente e segura, aproveitando o sistema de tipos do Rust para evitar erros comuns, como acessar índices fora dos limites.

Tuplas e desestruturação

Uma tupla é uma coleção de valores de tipos potencialmente diferentes, agrupados entre parênteses. O tamanho da tupla é fixo e não pode ser alterado após a declaração. Tuplas são úteis para retornar múltiplos valores de uma função ou para agrupar dados relacionados de forma leve.

Exemplo de criação de tupla:

let tup: (i32, f64, u8) = (500, 6.4, 1);

Para acessar os elementos individuais de uma tupla, podemos usar a desestruturação, que quebra a tupla em variáveis separadas, ou a notação de ponto com o índice:

let tup = (500, 6.4, 1);
let (x, y, z) = tup; // desestruturação
println!("Valor de y: {y}"); // 6.4

let primeiro = tup.0; // acesso por índice
println!("Primeiro elemento: {primeiro}");

A desestruturação é uma forma concisa e expressiva de extrair valores. Também é possível ignorar alguns valores usando _:

let tup = (1, 2, 3);
let (_, segundo, _) = tup;
println!("Segundo: {segundo}");

Tuplas também podem ser aninhadas e usadas em expressões match. Um padrão comum é retornar tuplas de funções:

fn calcula(x: i32) -> (i32, i32) {
    (x * 2, x * 3)
}

let (dobro, triplo) = calcula(5);
println!("Dobro: {dobro}, Triplo: {triplo}");

Arrays de tamanho fixo

Um array em Rust é uma coleção de elementos do mesmo tipo, com tamanho fixo conhecido em tempo de compilação. Eles são alocados na pilha (stack) e são eficientes para armazenar dados que não precisam crescer. A sintaxe é [tipo; tamanho].

Exemplo de declaração e inicialização:

let a: [i32; 5] = [1, 2, 3, 4, 5]; // array com 5 inteiros
let b = [3; 5]; // [3, 3, 3, 3, 3] (5 elementos iguais a 3)

Arrays são úteis quando você sabe exatamente quantos elementos precisa, como dias da semana ou coordenadas fixas. Eles são diferentes de vetores (Vec), que são dinâmicos e alocados no heap.

Para acessar elementos, usamos índices entre colchetes:

let primeiro = a[0];
let ultimo = a[4];
println!("Primeiro: {primeiro}, Último: {ultimo}");

O compilador verifica se o índice está dentro dos limites? Não, a verificação é feita em tempo de execução. Se você acessar um índice inválido, o programa entra em pânico (panic). Veremos isso na próxima seção.

Acesso e limites

Em Rust, o acesso a elementos de array (e tupla) é seguro quanto a limites? Para arrays, sim, em tempo de execução. Se você tentar acessar um índice fora do intervalo, o Rust causa um panic (pânico) e o programa aborta, evitando comportamento indefinido.

Exemplo de acesso inválido:

let a = [1, 2, 3];
let index = 10;
let element = a[index]; // isso vai causar panic em tempo de execução
println!("Elemento: {element}");

Para evitar panics, você deve garantir que os índices estejam dentro dos limites. Uma prática comum é usar métodos como get() que retornam um Option:

let a = [1, 2, 3];
let index = 10;
match a.get(index) {
    Some(valor) => println!("Valor: {valor}"),
    None => println!("Índice fora dos limites"),
}

Para tuplas, o acesso por índice (tup.0) é verificado em tempo de compilação: se você usar um índice que não existe, o compilador rejeita. Já para arrays, o índice é uma expressão, então a verificação é em tempo de execução.

Podemos iterar sobre arrays com loops seguros:

let a = [10, 20, 30];
for element in a {
    println!("Valor: {element}");
}

Ou com índices:

for i in 0..a.len() {
    println!("a[{i}] = {}", a[i]);
}

O método len() retorna o tamanho do array, que é conhecido em tempo de compilação.

Boas práticas

  • Use tuplas para agrupar poucos valores de tipos diferentes quando não quiser criar uma struct.
  • Prefira arrays quando o tamanho for fixo e conhecido; para coleções dinâmicas, use Vec.
  • Sempre verifique índices dinâmicos antes de acessar arrays; use get() para evitar panics.
  • Aproveite a desestruturação para tornar o código mais legível.

Referências

Exercícios

  1. Crie uma tupla chamada ponto contendo dois valores: x (i32) e y (i32). Em seguida, desestruture-a em duas variáveis e imprima-as.

    ✓ Resposta:
    let ponto: (i32, i32) = (10, 20);
    let (x, y) = ponto;
    println!("x: {x}, y: {y}");
  2. Declare um array de 4 inteiros: [1, 2, 3, 4]. Acesse o terceiro elemento (índice 2) e imprima-o. Depois, tente acessar um índice inválido (por exemplo, 5) e veja o que acontece. (Não precisa mostrar o panic, apenas descreva.)

    ✓ Resposta:
    let arr = [1, 2, 3, 4];
    println!("Terceiro elemento: {}", arr[2]);
    // arr[5] causaria panic em tempo de execução: index out of bounds
  3. Escreva uma função que recebe uma tupla (i32, f64) e retorna uma nova tupla com os valores trocados (f64, i32).

    ✓ Resposta:
    fn troca(t: (i32, f64)) -> (f64, i32) {
        (t.1, t.0)
    }
    
    fn main() {
        let original = (5, 3.14);
        let trocada = troca(original);
        println!("Trocada: {:?}, {:?}", trocada.0, trocada.1);
    }
  4. Crie um array de 5 elementos com o valor inicial 0 usando a sintaxe de inicialização rápida. Em seguida, preencha cada posição com o quadrado do índice (i*i) usando um loop.

    ✓ Resposta:
    let mut arr = [0; 5];
    for i in 0..arr.len() {
        arr[i] = (i * i) as i32;
    }
    println!("{:?}", arr); // [0, 1, 4, 9, 16]
  5. Use o método get() para acessar o índice 2 de um array [10, 20, 30] e o índice 5. Imprima o valor ou uma mensagem de erro conforme o caso.

    ✓ Resposta:
    let arr = [10, 20, 30];
    for &idx in &[2, 5] {
        match arr.get(idx) {
            Some(val) => println!("arr[{}] = {}", idx, val),
            None => println!("Índice {} está fora dos limites", idx),
        }
    }