HashMap é uma estrutura de dados fundamental em Rust que armazena pares chave-valor, permitindo acesso rápido aos valores a partir de suas chaves. Diferente de arrays ou vetores, onde os índices são numéricos, HashMap permite usar qualquer tipo que implemente as traits Hash e Eq como chave. Isso o torna ideal para situações como dicionários, caches, contagens de frequência e muito mais.

Nesta aula, vamos explorar as operações básicas de inserção e leitura, a conveniente entry API que simplifica padrões comuns, as regras de ownership que garantem segurança de memória, e como iterar sobre pares chave-valor. Ao final, você terá domínio prático para usar HashMap em seus projetos Rust.

Inserção e leitura

Para usar HashMap, primeiro importamos a estrutura do módulo std::collections. A inserção é feita com o método insert, que recebe uma chave e um valor. Se a chave já existir, o valor antigo é substituído e o método retorna Some(valor_antigo); caso contrário, retorna None.

Para leitura, usamos get, que retorna um Option<&V>. Isso significa que podemos acessar o valor por referência, sem tomar posse dele. Se a chave não existir, recebemos None. Outra forma comum é usar [] (indexação), mas ela pode causar pânico se a chave não existir, então é mais seguro usar get.

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();

    // Inserção
    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);

    // Leitura com get
    let team_name = String::from("Blue");
    let score = scores.get(&team_name); // Option<&i32>
    match score {
        Some(value) => println!("Pontuação do Blue: {}", value),
        None => println!("Time não encontrado"),
    }

    // Leitura com indexação (cuidado: pode causar pânico)
    // println!("{}", scores[&String::from("Blue")]);
}

Note que insert e get usam referências para chaves. O método entry (próximo tópico) também é muito útil para inserções condicionais.

entry API

A entry API é uma das ferramentas mais elegantes do HashMap. Ela permite verificar se uma chave existe e, dependendo do caso, inserir, atualizar ou modificar o valor de forma eficiente, evitando múltiplas buscas. O método entry recebe uma chave e retorna um enum Entry, que pode ser Occupied ou Vacant.

Combinando com or_insert, or_insert_with ou and_modify, podemos escrever código conciso e seguro. Por exemplo, para contar palavras em um texto, usamos entry para inserir 0 se a palavra não existir e depois incrementar.

use std::collections::HashMap;

fn main() {
    let text = "hello world hello";
    let mut word_counts = HashMap::new();

    for word in text.split_whitespace() {
        let count = word_counts.entry(word).or_insert(0);
        *count += 1;
    }

    println!("{:?}", word_counts); // {"hello": 2, "world": 1}
}

O método or_insert retorna uma referência mutável ao valor (se a chave existia, retorna referência ao valor existente; se não, insere o valor padrão e retorna referência a ele). Assim, podemos modificar o valor diretamente. Outros métodos úteis: or_insert_with (para valores computados sob demanda) e and_modify (para modificar o valor existente antes de inserir o padrão).

Ownership em HashMap

HashMap possui regras de ownership rigorosas. Quando você insere um valor, o HashMap toma posse dele (a menos que seja uma referência, mas isso exige lifetimes). Para tipos que implementam Copy (como inteiros), eles são copiados; para tipos como String, a propriedade é transferida.

Isso significa que, após inserir, você não pode mais usar a variável original se ela não for Copy. Além disso, ao usar get, você obtém uma referência ao valor, mas não pode modificar o HashMap enquanto essa referência existir (regra de empréstimo). O exemplo a seguir ilustra esses conceitos.

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    let key = String::from("chave");
    let value = String::from("valor");

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

    // Para evitar mover, podemos inserir referências, mas isso exige anotações de lifetime
    let key2 = String::from("chave2");
    let value2 = String::from("valor2");
    map.insert(&key2, &value2); // agora map tem referências; key2 e value2 permanecem vivos

    // Ao usar get, obtemos referência;
    let val_ref = map.get(&key2); // Option<&&String>
    // map.insert(String::from("outra"), String::from("x")); // erro se val_ref ainda estiver em escopo (empréstimo)
}

Entender essas regras é crucial para evitar erros de compilação. Sempre que possível, use tipos Copy para chaves e valores, ou planeje a posse dos dados.

Iteração

Podemos iterar sobre um HashMap de várias formas: sobre pares (chave, valor), apenas chaves ou apenas valores. Os métodos iter, keys, values e into_iter (consome o HashMap) estão disponíveis. A ordem de iteração é arbitrária, não garantindo nenhuma ordem específica.

Para iterar sem consumir o HashMap, use iter() que retorna um iterador de tuplas (&K, &V). Se quiser modificar os valores durante a iteração, use iter_mut(). Para consumir o HashMap, use into_iter() que move os pares para o iterador.

use std::collections::HashMap;

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

    // Iteração sobre pares (referências)
    for (key, value) in map.iter() {
        println!("{}: {}", key, value);
    }

    // Iteração e modificação de valores
    for (_, value) in map.iter_mut() {
        *value *= 10;
    }

    // Iteração consumindo o HashMap
    for (key, value) in map.into_iter() {
        println!("{}: {}", key, value);
    }
    // map não pode mais ser usado
}

Lembre-se: a iteração não garante ordem. Se precisar de ordem, considere usar BTreeMap (que ordena pelas chaves) ou coletar os pares em um vetor e ordená-los.

Boas práticas

  • Prefira entry em vez de get + insert para evitar múltiplas buscas.
  • Use tipos de chave que implementam Hash e Eq (como String, i32, tuplas). Evite tipos que mudam de hash (como vetores mutáveis).
  • Para grandes HashMap, defina uma capacidade inicial com HashMap::with_capacity(n) para evitar realocações.
  • Se precisar de ordem, considere BTreeMap.

Referências

Exercícios

  1. Crie um HashMap que mapeia nomes de times a pontuações. Insira os times "Red" com 30 pontos e "Blue" com 50. Depois, leia a pontuação do "Blue" e imprima.

    ✓ Resposta:
    use std::collections::HashMap;
    
    fn main() {
        let mut scores = HashMap::new();
        scores.insert("Red", 30);
        scores.insert("Blue", 50);
        if let Some(score) = scores.get("Blue") {
            println!("Pontuação do Blue: {}", score);
        }
    }
  2. Use a entry API para contar a frequência de cada letra em uma string "aabbc".

    ✓ Resposta:
    use std::collections::HashMap;
    
    fn main() {
        let s = "aabbc";
        let mut map = HashMap::new();
        for ch in s.chars() {
            *map.entry(ch).or_insert(0) += 1;
        }
        println!("{:?}", map); // {'a': 2, 'b': 2, 'c': 1}
    }
  3. Explique por que o código a seguir não compila e corrija-o: let mut map = HashMap::new(); let s = String::from("key"); map.insert(s, 1); println!("{}", s);

    ✓ Resposta:

    O erro ocorre porque s é movido para o HashMap no insert, então não pode mais ser usado. Para corrigir, podemos clonar s ou usar uma referência com lifetime adequado. Exemplo corrigido (clonando): map.insert(s.clone(), 1); println!("{}", s);.

  4. Itere sobre o HashMap map (contendo pares &str -> i32) e imprima cada chave e valor, depois multiplique cada valor por 2 usando iter_mut.

    ✓ Resposta:
    use std::collections::HashMap;
    
    fn main() {
        let mut map = HashMap::new();
        map.insert("a", 1);
        map.insert("b", 2);
        for (k, v) in map.iter() {
            println!("{}: {}", k, v);
        }
        for (_, v) in map.iter_mut() {
            *v *= 2;
        }
        println!("{:?}", map); // {"a": 2, "b": 4}
    }
  5. Escreva uma função que recebe um vetor de números inteiros e retorna um HashMap mapeando cada número à sua frequência.

    ✓ Resposta:
    use std::collections::HashMap;
    
    fn frequencias(nums: &[i32]) -> HashMap<i32, usize> {
        let mut map = HashMap::new();
        for &n in nums {
            *map.entry(n).or_insert(0) += 1;
        }
        map
    }
    
    fn main() {
        let nums = vec![1, 2, 2, 3, 3, 3];
        let freq = frequencias(&nums);
        println!("{:?}", freq); // {1: 1, 2: 2, 3: 3}
    }