O HashMap<K, V> é uma das coleções mais úteis da biblioteca padrão do Rust. Ele permite armazenar dados associando uma chave única a um valor, oferecendo operações de inserção, busca e remoção com complexidade média O(1). Nesta aula, vamos aprender como usar o HashMap de forma eficiente, desde operações básicas até técnicas avançadas como a Entry API, além de entender como o sistema de ownership interage com essa estrutura.

O HashMap é implementado como uma tabela hash, onde as chaves são hasheadas para determinar a posição de armazenamento. Isso exige que as chaves implementem as traits Eq e Hash, o que é automático para tipos simples como String e inteiros. O valor pode ser qualquer tipo. Vamos começar com operações fundamentais.

Inserção e leitura

Para inserir um par chave-valor, usamos o método insert. Se a chave já existir, o valor antigo é substituído e o valor antigo é retornado (como Option<V>). Para ler um valor, usamos get, que retorna Option<&V>. Existe também get_mut que retorna uma referência mutável.

use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

let team_name = String::from("Blue");
let score = scores.get(&team_name).copied().unwrap_or(0);
println!("Score: {}", score); // 10

// Usando get_mut
if let Some(value) = scores.get_mut(&String::from("Yellow")) {
    *value += 10;
}
println!("Yellow: {:?}", scores.get(&String::from("Yellow"))); // Some(60)

Observe que get retorna uma referência. Para obter o valor por cópia, usamos copied (se o valor implementa Copy) ou cloned. A função unwrap_or fornece um valor padrão caso a chave não exista.

Entry API

A Entry API é uma ferramenta poderosa para lidar com a presença ou ausência de uma chave de forma concisa e eficiente. Em vez de verificar se a chave existe e depois inserir, podemos usar uma entrada (Entry) que representa o estado de uma chave no mapa. O método entry retorna um enum Entry, que pode ser Occupied ou Vacant. A partir daí, podemos usar métodos como or_insert, or_insert_with, and_modify, entre outros.

use std::collections::HashMap;

let mut map = HashMap::new();
map.insert("a", 1);

// or_insert: insere um valor padrão se a chave não existir
map.entry("b").or_insert(2);
map.entry("a").or_insert(10); // não modifica, pois "a" já existe
println!("{:?}", map); // {"a": 1, "b": 2}

// or_insert_with: insere usando um closure
map.entry("c").or_insert_with(|| 3);

// and_modify: modifica o valor se a chave existir
map.entry("a").and_modify(|v| *v += 10);
println!("{:?}", map); // {"a": 11, "b": 2, "c": 3}

// Combinação: or_insert_with e and_modify
map.entry("d").and_modify(|v| *v += 10).or_insert(4);
println!("{:?}", map); // {"a": 11, "b": 2, "c": 3, "d": 4}

A Entry API evita múltiplas buscas e torna o código mais legível. É especialmente útil para contadores ou agregações.

Ownership em HashMap

O HashMap assume ownership das chaves e valores inseridos. Isso significa que, ao inserir uma variável, ela é movida para dentro do HashMap, e não podemos mais usá-la a menos que ela implemente Copy. Para tipos como String, a posse é transferida. Se quisermos usar referências, precisamos garantir que as referências sejam válidas durante toda a vida útil do HashMap, o que geralmente requer lifetimes explícitos.

use std::collections::HashMap;

let mut map = HashMap::new();
let key = String::from("key");
let value = String::from("value");

map.insert(key, value); // key e value são movidos
// println!("{}", key); // erro: key foi movido

// Para usar referências, precisamos de lifetimes
let name = String::from("Alice");
let mut map_ref = HashMap::new();
map_ref.insert(&name, 42); // referência a name
println!("{}", name); // ainda válido, pois map_ref não possui ownership
// Mas cuidado: name precisa viver mais que map_ref

Outro ponto importante: se você tentar acessar uma chave que não existe, o HashMap não a criará automaticamente. Use a Entry API ou métodos como get que retornam Option.

Iteração

O HashMap fornece três formas principais de iteração: sobre pares (chave, valor), apenas chaves, ou apenas valores. Os iteradores retornam referências aos itens. Para modificar valores durante a iteração, use iter_mut.

use std::collections::HashMap;

let mut map = HashMap::new();
map.insert("a", 1);
map.insert("b", 2);
map.insert("c", 3);

// Iterar sobre pares (chave, valor) - imutável
for (key, value) in &map {
    println!("{}: {}", key, value);
}

// Iterar sobre chaves
for key in map.keys() {
    println!("Key: {}", key);
}

// Iterar sobre valores (imutável)
for val in map.values() {
    println!("Val: {}", val);
}

// Iterar mutável sobre valores
for val in map.values_mut() {
    *val += 10;
}
println!("{:?}", map); // {"a": 11, "b": 12, "c": 13}

// Consumir o HashMap (move os valores)
for (key, value) in map {
    println!("{}: {}", key, value);
}
// map não pode mais ser usado

Note que a ordem de iteração não é garantida. Se precisar de ordem, considere usar BTreeMap.

Boas práticas

  • Prefira a Entry API para evitar múltiplas buscas.
  • Use with_capacity se souber o número aproximado de elementos para evitar realocações.
  • Para chaves compostas, considere usar tuplas ou structs que implementem Hash e Eq.
  • Lembre-se de que o HashMap não é ordenado; se precisar de ordem, use BTreeMap.

Referências

Exercícios

  1. Crie um HashMap que mapeie nomes de frutas a suas quantidades. Insira três frutas e depois atualize a quantidade de uma delas usando a Entry API.

    ✓ Resposta:
    use std::collections::HashMap;
    
    let mut frutas = HashMap::new();
    frutas.insert("maçã", 5);
    frutas.insert("banana", 3);
    frutas.insert("laranja", 7);
    
    frutas.entry("banana").and_modify(|q| *q += 2).or_insert(0);
    println!("{:?}", frutas); // {"maçã": 5, "banana": 5, "laranja": 7}
  2. Escreva uma função que receba um HashMap de String para i32 e retorne a soma de todos os valores. Use iteração.

    ✓ Resposta:
    fn soma_valores(map: &HashMap<String, i32>) -> i32 {
        map.values().sum()
    }
    
    let mut m = HashMap::new();
    m.insert("a".to_string(), 10);
    m.insert("b".to_string(), 20);
    println!("{}", soma_valores(&m)); // 30
  3. Dado um vetor de strings, conte quantas vezes cada string aparece usando um HashMap e a Entry API.

    ✓ Resposta:
    fn contar(palavras: &[&str]) -> HashMap<&str, u32> {
        let mut contagem = HashMap::new();
        for palavra in palavras {
            *contagem.entry(palavra).or_insert(0) += 1;
        }
        contagem
    }
    
    let palavras = vec!["a", "b", "a", "c", "b", "a"];
    println!("{:?}", contar(&palavras)); // {"a": 3, "b": 2, "c": 1}
  4. Explique por que o código abaixo não compila e como corrigi-lo:
    let mut map = HashMap::new(); let key = String::from("k"); map.insert(key, 1); println!("{}", key);

    ✓ Resposta:

    Não compila porque insert move a chave para dentro do HashMap. A variável key não pode mais ser usada. Para corrigir, podemos clonar a chave antes de inserir ou usar uma referência com lifetime apropriado. Exemplo corrigido com clone: map.insert(key.clone(), 1); println!("{}", key);

  5. Crie um HashMap que armazene o preço de produtos (nome: preço). Use iteração mutável para aplicar um desconto de 10% em todos os produtos com preço acima de 50.

    ✓ Resposta:
    use std::collections::HashMap;
    
    let mut precos = HashMap::new();
    precos.insert("tv".to_string(), 2000);
    precos.insert("mouse".to_string(), 30);
    precos.insert("teclado".to_string(), 80);
    
    for preco in precos.values_mut() {
        if *preco > 50 {
            *preco = (*preco as f64 * 0.9) as i32;
        }
    }
    println!("{:?}", precos); // {"tv": 1800, "mouse": 30, "teclado": 72}