HashMaps e suas operações

1. Introdução ao HashMap

O HashMap<K, V> é uma estrutura de dados que armazena pares chave-valor, permitindo inserção, busca e remoção com complexidade média O(1). É ideal para situações onde você precisa associar valores a chaves únicas e realizar consultas rápidas.

Diferentemente do BTreeMap (que mantém as chaves ordenadas) ou de vetores e slices (que usam índices numéricos), o HashMap não garante nenhuma ordem específica para seus elementos. A escolha entre HashMap e BTreeMap depende da necessidade de ordenação: se você precisa iterar em ordem crescente das chaves, use BTreeMap; caso contrário, HashMap oferece melhor desempenho médio.

Para usar HashMap, é necessário importá-lo do módulo std::collections:

use std::collections::HashMap;

2. Criando e populando um HashMap

A forma mais simples de criar um HashMap é com HashMap::new():

let mut capitais = HashMap::new();
capitais.insert("Brasil", "Brasília");
capitais.insert("França", "Paris");
capitais.insert("Japão", "Tóquio");

Se você sabe quantos elementos serão inseridos, use with_capacity() para evitar realocações desnecessárias:

let mut idades: HashMap<String, u32> = HashMap::with_capacity(100);

HashMap também pode ser inicializado a partir de iteradores usando collect():

let paises = vec!["Brasil", "França", "Japão"];
let cidades = vec!["Brasília", "Paris", "Tóquio"];
let mapa: HashMap<&str, &str> = paises.into_iter().zip(cidades.into_iter()).collect();

3. Acessando valores

O método get() retorna Option<&V>, permitindo lidar com chaves ausentes sem pânico:

let mut mapa = HashMap::new();
mapa.insert("chave1", 42);

match mapa.get("chave1") {
    Some(valor) => println!("Valor: {}", valor),
    None => println!("Chave não encontrada"),
}

Para modificar o valor, use get_mut():

if let Some(valor) = mapa.get_mut("chave1") {
    *valor += 1;
}

O operador [] também pode ser usado, mas causa pânico se a chave não existir:

let valor = mapa["chave1"]; // Seguro se a chave existir
// let valor = mapa["chave_inexistente"]; // PÂNICO!

A API entry() é poderosa para acesso condicional:

mapa.entry("chave2").or_insert(0);

4. Atualizando e removendo entradas

insert() sobrescreve o valor existente e retorna o valor antigo (como Option):

let mut mapa = HashMap::new();
mapa.insert("a", 1);
let antigo = mapa.insert("a", 2); // antigo = Some(1)

Para remover uma chave, use remove():

mapa.remove("a"); // Remove a entrada e retorna o valor (Option)

remove_entry() retorna o par chave-valor removido:

if let Some((chave, valor)) = mapa.remove_entry("a") {
    println!("Removido: {} -> {}", chave, valor);
}

A combinação entry().or_insert() com and_modify() permite atualizações idiomáticas:

let mut contagem: HashMap<&str, u32> = HashMap::new();
let palavras = vec!["sol", "lua", "sol", "estrela", "sol"];

for palavra in &palavras {
    contagem.entry(palavra)
        .and_modify(|e| *e += 1)
        .or_insert(1);
}
// Resultado: {"sol": 3, "lua": 1, "estrela": 1}

5. Iteração sobre HashMaps

iter() produz pares (&K, &V) sem consumir o HashMap:

for (chave, valor) in &mapa {
    println!("{}: {}", chave, valor);
}

iter_mut() permite modificar valores durante a iteração:

for (_, valor) in &mut mapa {
    *valor *= 2;
}

into_iter() consome o HashMap, transferindo ownership:

let pares: Vec<(String, i32)> = mapa.into_iter().collect();

Para acessar apenas chaves ou valores:

for chave in mapa.keys() {
    println!("Chave: {}", chave);
}

for valor in mapa.values() {
    println!("Valor: {}", valor);
}

6. Propriedades de desempenho e hashing

O HashMap do Rust usa uma função de hash criptográfica (SipHash) por padrão, que oferece proteção contra ataques de colisão. O fator de carga padrão é 0.9, e quando excedido, o HashMap realiza um rehashing automático (redimensionamento).

Para que um tipo seja usado como chave, ele precisa implementar as traits Hash e Eq:

#[derive(Hash, Eq, PartialEq)]
struct Pessoa {
    nome: String,
    idade: u32,
}

Se você precisa de máximo desempenho e não se preocupa com ataques de colisão, pode usar um hasher mais rápido como fnv:

use std::collections::HashMap;
use fnv::FnvHashMap;

let mut mapa: FnvHashMap<String, i32> = FnvHashMap::default();

7. Operações avançadas

entry().or_insert_with() permite inicialização lazy com uma closure:

mapa.entry("caro".to_string())
    .or_insert_with(|| {
        println!("Calculando valor caro...");
        42
    });

Para combinar dois HashMaps, use extend():

let mut mapa1: HashMap<&str, i32> = [("a", 1), ("b", 2)].into_iter().collect();
let mapa2: HashMap<&str, i32> = [("b", 3), ("c", 4)].into_iter().collect();

mapa1.extend(mapa2);
// mapa1 agora contém: {"a": 1, "b": 3, "c": 4}

retain() filtra elementos condicionalmente (disponível desde Rust 1.18):

let mut mapa: HashMap<&str, i32> = [("a", 1), ("b", 2), ("c", 3)].into_iter().collect();
mapa.retain(|_, valor| *valor > 1);
// mapa agora contém: {"b": 2, "c": 3}

8. Boas práticas e armadilhas comuns

Ownership: HashMap exige que chaves e valores tenham ownership. Strings literais (&str) funcionam como chaves porque implementam Hash e Eq, mas se você tentar inserir uma referência temporária, o borrow checker impedirá:

let mut mapa = HashMap::new();
let chave = String::from("temp");
mapa.insert(&chave, 1); // Erro: chave não vive o suficiente
drop(chave); // mapa ainda contém referência inválida

Comparação com outras linguagens: Em Python, dicts são mais flexíveis (aceitam qualquer hashable). Em Java, HashMap requer implementação de hashCode() e equals(). Rust é mais explícito, exigindo traits Hash e Eq.

Quando evitar HashMap:
- Para menos de ~20 elementos, um vetor ou Vec<(K, V)> pode ser mais rápido
- Se precisar de ordenação, use BTreeMap
- Se as chaves são números inteiros consecutivos, use um vetor indexado

Boas práticas:
- Sempre use entry() para evitar duplicação de lookup
- Prefira with_capacity() quando o tamanho for conhecido
- Evite o operador [] para leitura a menos que tenha certeza da existência da chave
- Para chaves complexas, derive Hash e Eq automaticamente com #[derive]

Referências