Rust

Coleções — Vec, HashMap e HashSet na Prática Já leu

10 min de leitura

Coleções — Vec, HashMap e HashSet na Prática
  Até agora trabalhamos com dados de tamanho fixo — arrays, tuplas, tipos primitivos. Mas programas reais precisam de coleções

 

Até agora trabalhamos com dados de tamanho fixo — arrays, tuplas, tipos primitivos. Mas programas reais precisam de coleções que crescem e encolhem em tempo de execução: listas de usuários, índices de palavras, conjuntos de permissões. A biblioteca padrão de Rust oferece coleções ricas e eficientes. Neste artigo, vamos explorar as três mais usadas: Vec<T>, HashMap<K, V> e HashSet<T>.


Vec<T> — a lista dinâmica

Vec<T> é provavelmente a coleção mais usada em Rust. É uma lista de elementos do mesmo tipo, armazenada contiguamente na memória, que cresce dinamicamente conforme necessário.

Criando vetores

fn main() {
    // Vetor vazio com tipo explícito
    let mut numeros: Vec<i32> = Vec::new();

    // Vetor criado com a macro vec!
    let frutas = vec!["maçã", "banana", "laranja"];

    // Adicionando elementos
    numeros.push(10);
    numeros.push(20);
    numeros.push(30);

    println!("{:?}", numeros); // [10, 20, 30]
    println!("{:?}", frutas);  // ["maçã", "banana", "laranja"]
}

A macro vec! é um atalho conveniente para criar vetores com valores iniciais. O tipo é inferido automaticamente a partir dos elementos.

Acessando elementos

fn main() {
    let v = vec![10, 20, 30, 40, 50];

    // Acesso por índice — panic se fora dos limites
    let terceiro = v[2];
    println!("Terceiro: {terceiro}");

    // Acesso seguro com get — retorna Option
    match v.get(10) {
        Some(valor) => println!("Valor: {valor}"),
        None        => println!("Índice fora dos limites"),
    }

    // Primeiro e último
    println!("Primeiro: {:?}", v.first());
    println!("Último: {:?}", v.last());
}

A distinção entre v[i] e v.get(i) é importante: v[i] entra em panic se o índice for inválido, enquanto v.get(i) retorna Option<&T> — seguro por natureza. Em código robusto, prefira get quando o índice vem de entrada externa.

Iterando sobre vetores

fn main() {
    let mut precos = vec![29.90, 49.90, 15.50, 89.00];

    // Iteração por referência imutável
    println!("Preços:");
    for preco in &precos {
        println!("  R$ {preco:.2}");
    }

    // Iteração por referência mutável
    for preco in &mut precos {
        *preco *= 0.9; // 10% de desconto
    }

    println!("\nCom desconto:");
    for preco in &precos {
        println!("  R$ {preco:.2}");
    }
}

Note o *preco na iteração mutável — o asterisco desreferencia o ponteiro para acessar o valor subjacente. Sem ele, estaríamos tentando multiplicar a referência, não o valor.

Métodos essenciais de Vec

fn main() {
    let mut v = vec![3, 1, 4, 1, 5, 9, 2, 6, 5, 3];

    println!("Tamanho: {}", v.len());
    println!("Vazio? {}", v.is_empty());

    v.sort();
    println!("Ordenado: {:?}", v);

    v.dedup(); // remove duplicatas consecutivas
    println!("Sem duplicatas: {:?}", v);

    v.retain(|&x| x > 3); // mantém apenas elementos > 3
    println!("Apenas > 3: {:?}", v);

    let removido = v.pop(); // remove e retorna o último
    println!("Removido: {:?}", removido);
    println!("Restante: {:?}", v);

    // Concatenando vetores
    let mut a = vec![1, 2, 3];
    let b = vec![4, 5, 6];
    a.extend(b);
    println!("Concatenado: {:?}", a);
}

Saída:

Tamanho: 10
Vazio? false
Ordenado: [1, 1, 2, 3, 3, 4, 5, 5, 6, 9]
Sem duplicatas: [1, 2, 3, 4, 5, 6, 9]
Apenas > 3: [4, 5, 6, 9]
Removido: Some(9)
Restante: [4, 5, 6]
Concatenado: [1, 2, 3, 4, 5, 6]

Vec com enum — coleções heterogêneas

Vec<T> exige que todos os elementos sejam do mesmo tipo. Mas e se você precisar de tipos diferentes? Use um enum:

#[derive(Debug)]
enum Celula {
    Inteiro(i64),
    Decimal(f64),
    Texto(String),
    Vazio,
}

fn main() {
    let linha: Vec<Celula> = vec![
        Celula::Texto(String::from("Ana Silva")),
        Celula::Inteiro(30),
        Celula::Decimal(1.68),
        Celula::Vazio,
    ];

    for celula in &linha {
        match celula {
            Celula::Inteiro(v) => print!("{v:>10} "),
            Celula::Decimal(v) => print!("{v:>10.2} "),
            Celula::Texto(v)   => print!("{v:>10} "),
            Celula::Vazio      => print!("{:>10} ", "—"),
        }
    }
    println!();
}

Essa técnica é muito usada para representar linhas de planilhas, células de tabelas, ou qualquer estrutura tabular com tipos mistos.


HashMap<K, V> — o dicionário de Rust

HashMap<K, V> mapeia chaves do tipo K para valores do tipo V. Internamente usa hashing para acesso em tempo médio O(1).

Criando e populando

use std::collections::HashMap;

fn main() {
    let mut estoque: HashMap<String, u32> = HashMap::new();

    estoque.insert(String::from("maçã"),   150);
    estoque.insert(String::from("banana"),  80);
    estoque.insert(String::from("laranja"), 200);

    println!("{:?}", estoque);
}

Note o use std::collections::HashMap — diferente de Vec e Option, o HashMap não é importado automaticamente e precisa ser trazido ao escopo.

Acessando valores

use std::collections::HashMap;

fn main() {
    let mut capitais = HashMap::new();
    capitais.insert("Brasil",    "Brasília");
    capitais.insert("Argentina", "Buenos Aires");
    capitais.insert("Chile",     "Santiago");

    // get retorna Option<&V>
    match capitais.get("Brasil") {
        Some(capital) => println!("Capital do Brasil: {capital}"),
        None          => println!("País não encontrado"),
    }

    // Acesso direto — panic se não existir
    let capital = capitais["Argentina"];
    println!("Capital da Argentina: {capital}");

    // Verificando existência
    if capitais.contains_key("Chile") {
        println!("Chile está no mapa");
    }
}

Iterando sobre HashMap

use std::collections::HashMap;

fn main() {
    let mut notas = HashMap::new();
    notas.insert("Ana",    9.5);
    notas.insert("Carlos", 7.2);
    notas.insert("Maria",  8.8);

    // Iteração — ordem não garantida
    for (aluno, nota) in &notas {
        println!("{aluno}: {nota:.1}");
    }

    // Coletando e ordenando para exibição consistente
    let mut ranking: Vec<(&str, f64)> = notas
        .iter()
        .map(|(&k, &v)| (k, v))
        .collect();

    ranking.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());

    println!("\n── Ranking ──");
    for (i, (aluno, nota)) in ranking.iter().enumerate() {
        println!("{}. {aluno}: {nota:.1}", i + 1);
    }
}

entry — inserção condicional

Um padrão extremamente comum é: inserir um valor se a chave não existir, ou atualizar se existir. A API entry resolve isso elegantemente:

use std::collections::HashMap;

fn main() {
    let texto = "olá mundo olá rust mundo rust rust";
    let mut frequencia: HashMap<&str, u32> = HashMap::new();

    for palavra in texto.split_whitespace() {
        // entry retorna a entrada existente ou cria uma nova
        let contador = frequencia.entry(palavra).or_insert(0);
        *contador += 1;
    }

    let mut pares: Vec<(&&str, &u32)> = frequencia.iter().collect();
    pares.sort_by(|a, b| b.1.cmp(a.1));

    for (palavra, count) in pares {
        println!("{palavra}: {count}x");
    }
}

Saída:

rust: 3x
olá: 2x
mundo: 2x

entry(chave).or_insert(valor_padrão) é o padrão idiomático para contagem de frequência — um dos mais usados em código Rust real.


HashSet<T> — conjuntos sem duplicatas

HashSet<T> é uma coleção de valores únicos — sem chaves, sem ordem garantida, sem duplicatas. É como um HashMap onde só importa a presença ou ausência de um elemento.

Criando e usando

use std::collections::HashSet;

fn main() {
    let mut permissoes: HashSet<String> = HashSet::new();

    permissoes.insert(String::from("ler"));
    permissoes.insert(String::from("escrever"));
    permissoes.insert(String::from("executar"));
    permissoes.insert(String::from("ler")); // duplicata ignorada

    println!("Total de permissões: {}", permissoes.len()); // 3

    if permissoes.contains("escrever") {
        println!("Pode escrever.");
    }

    permissoes.remove("executar");
    println!("{:?}", permissoes);
}

Operações de conjunto

O verdadeiro poder de HashSet está nas operações matemáticas de conjuntos:

use std::collections::HashSet;

fn main() {
    let time_a: HashSet<&str> = ["Ana", "Carlos", "Maria", "João"]
        .iter().cloned().collect();

    let time_b: HashSet<&str> = ["Maria", "João", "Pedro", "Lucia"]
        .iter().cloned().collect();

    // Interseção — jogadores em ambos os times
    let em_ambos: HashSet<&&str> = time_a.intersection(&time_b).collect();
    println!("Em ambos: {:?}", em_ambos);

    // União — todos os jogadores
    let todos: HashSet<&&str> = time_a.union(&time_b).collect();
    println!("Total único: {}", todos.len());

    // Diferença — só no time A
    let apenas_a: HashSet<&&str> = time_a.difference(&time_b).collect();
    println!("Só no time A: {:?}", apenas_a);

    // Diferença simétrica — em um ou outro, mas não nos dois
    let exclusivos: HashSet<&&str> = time_a
        .symmetric_difference(&time_b)
        .collect();
    println!("Exclusivos: {:?}", exclusivos);
}

Um programa completo: análise de texto

Vamos combinar as três coleções num programa que analisa um texto e gera estatísticas:

use std::collections::{HashMap, HashSet};

fn analisar_texto(texto: &str) -> () {
    let palavras: Vec<&str> = texto
        .split_whitespace()
        .map(|p| p.trim_matches(|c: char| !c.is_alphabetic()))
        .filter(|p| !p.is_empty())
        .collect();

    let total = palavras.len();

    // Frequência com HashMap
    let mut frequencia: HashMap<&str, usize> = HashMap::new();
    for palavra in &palavras {
        *frequencia.entry(palavra).or_insert(0) += 1;
    }

    // Palavras únicas com HashSet
    let unicas: HashSet<&&str> = frequencia.keys().collect();

    // Top 5 mais frequentes
    let mut ranking: Vec<(&&str, &usize)> = frequencia.iter().collect();
    ranking.sort_by(|a, b| b.1.cmp(a.1));

    println!("── Análise de Texto ─────────────");
    println!("Total de palavras   : {total}");
    println!("Palavras únicas     : {}", unicas.len());
    println!("Riqueza vocabular   : {:.1}%",
        unicas.len() as f64 / total as f64 * 100.0);

    println!("\nTop 5 mais frequentes:");
    for (palavra, count) in ranking.iter().take(5) {
        let barra = "█".repeat(**count);
        println!("  {:12} {:3}x  {}", palavra, count, barra);
    }
}

fn main() {
    let texto = "Rust é uma linguagem de programação de sistemas \
                 que roda incrivelmente rápido previne falhas de \
                 segmentação e garante segurança de threads Rust \
                 é diferente de todas as outras linguagens Rust \
                 oferece controle de baixo nível com ergonomia \
                 de alto nível a comunidade Rust é acolhedora";

    analisar_texto(texto);
}

Saída:

── Análise de Texto ─────────────
Total de palavras   : 43
Palavras únicas     : 34
Riqueza vocabular   : 79.1%

Top 5 mais frequentes:
  Rust          3x  ███
  de            5x  █████
  é             3x  ███
  nível         2x  ██
  linguagem     1x  █

Ownership e coleções

As regras de ownership se aplicam integralmente às coleções. Quando você insere um valor numa coleção, ela toma posse dele:

use std::collections::HashMap;

fn main() {
    let chave = String::from("nome");
    let valor = String::from("Ana");

    let mut mapa = HashMap::new();
    mapa.insert(chave, valor);

    // println!("{chave}"); // ERRO: chave foi movida para o mapa
    // println!("{valor}"); // ERRO: valor foi movido para o mapa
}

Se quiser manter o acesso às strings originais, use referências — mas então o mapa só pode ser usado enquanto as referências forem válidas, e o compilador garantirá isso.


Escolhendo a coleção certa

A escolha entre coleções deve ser guiada pela necessidade:

Use Vec quando a ordem importa, quando você acessa elementos por índice, quando precisa iterar em sequência, ou quando simplesmente precisa de uma lista.

Use HashMap quando precisa associar chaves a valores e fazer buscas rápidas por chave — inventários, configurações, índices, caches.

Use HashSet quando precisa de unicidade, quando precisa verificar pertencimento rapidamente, ou quando quer fazer operações de conjunto como interseção e união.

A biblioteca padrão oferece ainda BTreeMap, BTreeSet, VecDeque, LinkedList e outras — cada uma com suas características de desempenho e ordenação. Mas Vec, HashMap e HashSet cobrem a vasta maioria dos casos de uso cotidianos.


Fontes e leituras recomendadas

Comentários

Mais em Rust

Funções, Expressões e Como Rust Pensa Diferente sobre Retorno de Valores
Funções, Expressões e Como Rust Pensa Diferente sobre Retorno de Valores

Se voc&ecirc; vem de Python, JavaScript ou Java, j&aacute; sabe o que &eacute...

Generics — Código que Funciona para Qualquer Tipo
Generics — Código que Funciona para Qualquer Tipo

&nbsp; No artigo anterior aprendemos que traits definem&nbsp;o que um tipo p...

Tratamento de Erros com Result — Erros como Valores, não Exceções
Tratamento de Erros com Result — Erros como Valores, não Exceções

&nbsp; Em linguagens como Java, Python e C#, erros s&atilde;o tratados com e...