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
- Documentação oficial do HashMap — Documentação completa da API HashMap com exemplos e explicações detalhadas
- Rust by Example: HashMap — Tutoriais práticos com exemplos de uso de HashMap em Rust
- The Rust Programming Language, Chapter 8.3: Hash Maps — Capítulo do livro oficial sobre HashMaps, parte da seção de coleções comuns
- HashMap in Rust: A Comprehensive Guide — Guia completo sobre HashMap em Rust, incluindo performance e boas práticas
- Rust Collections: Performance and Usage — Análise de desempenho das coleções Rust, incluindo comparações entre HashMap, BTreeMap e Vec
- Understanding Rust's HashMap — Artigo técnico explicando o funcionamento interno do HashMap em Rust