HashMap
Nesta aula, exploramos o HashMap em Rust, uma estrutura de dados para armazenar pares chave-valor. Abordamos inserção, leitura, a poderosa Entry API, questões de ownership e diferentes formas de iteração, com exemplos práticos e boas práticas.
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_refOutro 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 usadoNote 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_capacityse souber o número aproximado de elementos para evitar realocações. - Para chaves compostas, considere usar tuplas ou structs que implementem
HasheEq. - Lembre-se de que o HashMap não é ordenado; se precisar de ordem, use
BTreeMap.
Referências
- Documentação oficial do HashMap
- Capítulo 8.3 do Livro de Rust
- Documentação da Entry API
- Módulo std::hash
- Rust by Example: HashMap
Exercícios
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}Escreva uma função que receba um HashMap de
Stringparai32e 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)); // 30Dado 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}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
insertmove a chave para dentro do HashMap. A variávelkeynã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);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}