Vetores (Vec) são uma das estruturas de dados mais fundamentais e versáteis em Rust. Eles representam uma coleção contígua de elementos do mesmo tipo, armazenada na heap, que pode crescer ou encolher dinamicamente. Diferente de arrays ([T; N]), cujo tamanho é fixo em tempo de compilação, vetores são alocados dinamicamente e podem ter seu tamanho alterado em tempo de execução. Nesta aula, exploraremos desde a criação básica até tópicos avançados como capacidade e realocação.

Entender o funcionamento interno de Vec é crucial para escrever código eficiente e seguro. Rust fornece garantias de segurança de memória sem sacrificar performance, e o uso correto de vetores é um exemplo clássico disso. Vamos mergulhar nos detalhes.

Criação e push

Para criar um vetor vazio, usamos Vec::new() ou a macro vec![]. A macro vec! também permite inicializar o vetor com elementos. O método push adiciona um elemento ao final do vetor. Exemplo:

let mut v: Vec<i32> = Vec::new();
v.push(1);
v.push(2);
v.push(3);

let v2 = vec![4, 5, 6]; // usando a macro
println!("{:?}", v);  // [1, 2, 3]
println!("{:?}", v2); // [4, 5, 6]

Note que declaramos mut para poder modificar o vetor. push pode causar realocação se a capacidade for insuficiente. O tipo do vetor é inferido a partir dos elementos ou explicitamente anotado.

Indexação vs get

O acesso a elementos de um vetor pode ser feito de duas formas principais: indexação direta com v[index] e o método get(index). A indexação direta causa um panic se o índice estiver fora dos limites, enquanto get retorna Option<&T>, permitindo tratamento seguro.

let v = vec![10, 20, 30];

// Indexação direta (pode panic!)
let first = &v[0];
println!("Primeiro: {}", first);

// get retorna Option
match v.get(1) {
    Some(val) => println!("Segundo: {}", val),
    None => println!("Índice inválido"),
}

// Fora dos limites
// let out = &v[5]; // panic!
let safe = v.get(5);
match safe {
    Some(val) => println!("Valor: {}", val),
    None => println!("Índice 5 não existe"),
}

Recomenda-se usar get quando o índice pode ser inválido, e indexação direta apenas quando você tem certeza de que o índice é válido (por exemplo, após verificar o tamanho).

Iteração

Iterar sobre um vetor é simples usando loops for. Podemos iterar por referência imutável, mutável ou consumindo o vetor (tomando posse).

let v = vec![1, 2, 3];

// Iteração imutável (empresta &T)
for x in &v {
    println!("{}", x);
}

// Iteração mutável (empresta &mut T)
let mut v = vec![1, 2, 3];
for x in &mut v {
    *x += 10;
}
println!("{:?}", v); // [11, 12, 13]

// Consumindo o vetor (move para dentro do loop)
let v = vec![1, 2, 3];
for x in v {
    println!("{}", x);
}
// v não pode mais ser usado aqui

Além disso, podemos usar métodos de iteração como iter(), iter_mut(), into_iter(), e adaptadores como map, filter, etc.

Capacidade e realocação

Um vetor possui dois atributos importantes: len() (número de elementos) e capacity() (espaço alocado na heap). Quando push é chamado e a capacidade é insuficiente, o vetor realoca um novo buffer maior (geralmente dobrando a capacidade) e copia os elementos para lá. Isso pode ser custoso, então é útil pré-alocar capacidade com Vec::with_capacity.

let mut v = Vec::with_capacity(10);
println!("Capacidade inicial: {}", v.capacity()); // 10

for i in 0..10 {
    v.push(i);
}
println!("Capacidade após 10 pushes: {}", v.capacity()); // 10 (ainda)

v.push(11);
println!("Capacidade após 11º push: {}", v.capacity()); // 20 (dobrou)

// shrink_to_fit reduz a capacidade para o tamanho atual
v.shrink_to_fit();
println!("Capacidade após shrink: {}", v.capacity()); // 11

Entender a capacidade ajuda a otimizar o uso de memória e evitar realocações desnecessárias. Use with_capacity quando souber quantos elementos serão inseridos.

Boas práticas

  • Prefira vec![val; n] para inicializar com valores padrão.
  • Use Vec::with_capacity para evitar múltiplas realocações.
  • Sempre prefira get sobre indexação direta quando o índice for variável.
  • Evite manter referências ao vetor enquanto o modifica (regras de borrowing).
  • Considere usar slices (&[T]) para funções que só precisam ler os dados.

Referências

Exercícios

  1. Crie um vetor vazio de inteiros e adicione os números 1, 2 e 3 usando push. Depois imprima o vetor.

    ✓ Resposta:
    let mut v: Vec<i32> = Vec::new();
    v.push(1);
    v.push(2);
    v.push(3);
    println!("{:?}", v);
  2. Dado o vetor let v = vec![10, 20, 30, 40, 50];, acesse o elemento no índice 2 usando indexação direta e o elemento no índice 10 usando get. Imprima ambos de forma segura.

    ✓ Resposta:
    let v = vec![10, 20, 30, 40, 50];
    let idx2 = &v[2];
    println!("Índice 2: {}", idx2);
    match v.get(10) {
        Some(val) => println!("Índice 10: {}", val),
        None => println!("Índice 10 não existe"),
    }
  3. Itere sobre um vetor let mut v = vec![1, 2, 3]; e multiplique cada elemento por 2 (modificando o vetor). Depois imprima o vetor modificado.

    ✓ Resposta:
    let mut v = vec![1, 2, 3];
    for x in &mut v {
        *x *= 2;
    }
    println!("{:?}", v); // [2, 4, 6]
  4. Crie um vetor com capacidade para 5 elementos usando with_capacity. Adicione 5 elementos e verifique a capacidade. Em seguida, adicione um sexto elemento e veja como a capacidade muda.

    ✓ Resposta:
    let mut v = Vec::with_capacity(5);
    println!("Capacidade inicial: {}", v.capacity());
    for i in 0..5 {
        v.push(i);
    }
    println!("Capacidade após 5 pushes: {}", v.capacity());
    v.push(5);
    println!("Capacidade após 6º push: {}", v.capacity()); // geralmente dobra para 10
  5. Escreva uma função que recebe um vetor de inteiros e retorna a soma de todos os elementos. Use iteração por referência. Teste com vec![1, 2, 3].

    ✓ Resposta:
    fn soma(v: &Vec<i32>) -> i32 {
        let mut total = 0;
        for x in v {
            total += x;
        }
        total
    }
    
    fn main() {
        let v = vec![1, 2, 3];
        println!("Soma: {}", soma(&v));
    }