Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Armazenando Chaves com Valores Associados em Hash Maps

A última das nossas coleções comuns é o hash map. O tipo HashMap<K, V> armazena um mapeamento de chaves do tipo K para valores do tipo V usando uma hashing function, que determina como essas chaves e valores são organizados na memória. Muitas linguagens de programação oferecem esse tipo de estrutura de dados, mas frequentemente com nomes diferentes, como hash, map, object, hash table, dictionary ou associative array, para citar alguns.

Hash maps são úteis quando você quer procurar dados não por índice, como faz com vetores, mas por meio de uma chave que pode ser de qualquer tipo. Por exemplo, em um jogo, você pode acompanhar a pontuação de cada time em um hash map, em que cada chave é o nome de um time e os valores são suas respectivas pontuações. Dado o nome de um time, você pode recuperar sua pontuação.

Nesta seção, veremos a API básica de hash maps, mas há muito mais funcionalidades escondidas nas funções definidas em HashMap<K, V> pela biblioteca padrão. Como sempre, consulte a documentação da biblioteca padrão para mais informações.

Criando um Novo Hash Map

Uma forma de criar um hash map vazio é usar new e adicionar elementos com insert. Na Listagem 8-20, estamos acompanhando as pontuações de dois times, Blue e Yellow. O time Blue começa com 10 pontos, e o time Yellow começa com 50.

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);
}
Listing 8-20: Criando um novo hash map e inserindo algumas chaves e valores

Observe que primeiro precisamos usar use para trazer HashMap da parte de coleções da biblioteca padrão. Das nossas três coleções comuns, esta é a menos usada com frequência, então não faz parte dos itens colocados automaticamente em escopo pelo prelude. Hash maps também recebem menos suporte direto da biblioteca padrão; por exemplo, não existe uma macro embutida para construí-los.

Assim como vetores, hash maps armazenam seus dados na heap. Esse HashMap tem chaves do tipo String e valores do tipo i32. Como vetores, hash maps são homogêneos: todas as chaves precisam ter o mesmo tipo, e todos os valores também precisam ter o mesmo tipo.

Acessando Valores em um Hash Map

Podemos obter um valor de um hash map fornecendo sua chave ao método get, como mostra a Listagem 8-21.

fn main() {
    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);
}
Listing 8-21: Acessando a pontuação do time Blue armazenada no hash map

Aqui, score terá o valor associado ao time Blue, e o resultado será 10. O método get retorna um Option<&V>; se não houver valor para aquela chave no hash map, get retornará None. Este programa lida com o Option chamando copied para obter um Option<i32> em vez de um Option<&i32> e, em seguida, unwrap_or para definir score como zero caso scores não tenha uma entrada para aquela chave.

Podemos iterar sobre cada par chave-valor em um hash map de maneira parecida com o que fazemos com vetores, usando um loop for:

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();

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

    for (key, value) in &scores {
        println!("{key}: {value}");
    }
}

Esse código imprimirá cada par em uma ordem arbitrária:

Yellow: 50
Blue: 10

Gerenciando Ownership em Hash Maps

Para tipos que implementam a trait Copy, como i32, os valores são copiados para dentro do hash map. Para valores próprios, como String, os valores são movidos, e o hash map passa a ser o proprietário desses valores, como mostra a Listagem 8-22.

fn main() {
    use std::collections::HashMap;

    let field_name = String::from("Favorite color");
    let field_value = String::from("Blue");

    let mut map = HashMap::new();
    map.insert(field_name, field_value);
    // field_name and field_value are invalid at this point, try using them and
    // see what compiler error you get!
}
Listing 8-22: Mostrando que as chaves e os valores passam a ser de propriedade do hash map depois de inseridos

Não podemos mais usar as variáveis field_name e field_value depois que elas forem movidas para dentro do hash map pela chamada a insert.

Se inserirmos referências a valores no hash map, os valores em si não serão movidos para dentro dele. Os valores apontados por essas referências precisam continuar válidos por pelo menos tanto tempo quanto o hash map for válido. Falaremos mais sobre essas questões em “Validando Referências com Lifetimes” no Capítulo 10.

Atualizando um Hash Map

Embora o número de pares chave-valor possa crescer, cada chave única só pode ter um valor associado por vez, embora o inverso não seja verdadeiro. Por exemplo, tanto o time Blue quanto o time Yellow podem ter o valor 10 armazenado no hash map scores.

Quando você quer alterar os dados em um hash map, precisa decidir como tratar o caso em que uma chave já possui um valor associado. Você pode substituir o valor antigo pelo novo, ignorando completamente o valor anterior. Pode manter o valor antigo e ignorar o novo, adicionando o novo valor apenas se a chave ainda não tiver um valor. Ou pode combinar o valor antigo com o novo. Vamos ver como fazer cada uma dessas coisas!

Sobrescrevendo um Valor

Se inserirmos uma chave e um valor em um hash map e depois inserirmos essa mesma chave com um valor diferente, o valor associado a ela será substituído. Mesmo que o código da Listagem 8-23 chame insert duas vezes, o hash map conterá apenas um par chave-valor, porque estamos inserindo valor para a chave do time Blue nas duas vezes.

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();

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

    println!("{scores:?}");
}
Listing 8-23: Substituindo o valor armazenado em uma determinada chave

Esse código imprimirá {"Blue": 25}. O valor original, 10, foi sobrescrito.

Adicionando uma Chave e um Valor Somente se a Chave Ainda Não Estiver Presente

É comum verificar se uma determinada chave já existe no hash map com algum valor e então tomar as seguintes ações: se a chave já existir, o valor atual deve permanecer como está; se a chave não existir, inserimos a chave e um valor para ela.

Hash maps têm uma API especial para isso chamada entry, que recebe como parâmetro a chave que você quer verificar. O valor retornado pelo método entry é um enum chamado Entry, que representa um valor que pode ou não existir. Digamos que queremos verificar se a chave do time Yellow tem um valor associado. Se não tiver, queremos inserir o valor 50, e o mesmo vale para o time Blue. Usando a API entry, o código fica como na Listagem 8-24.

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();
    scores.insert(String::from("Blue"), 10);

    scores.entry(String::from("Yellow")).or_insert(50);
    scores.entry(String::from("Blue")).or_insert(50);

    println!("{scores:?}");
}
Listing 8-24: Usando o método entry para inserir apenas se a chave ainda não tiver um valor

O método or_insert em Entry foi definido para retornar uma referência mutável ao valor da chave correspondente caso essa chave exista; se não existir, ele insere o parâmetro como novo valor dessa chave e retorna uma referência mutável a esse novo valor. Essa técnica é bem mais limpa do que escrever toda a lógica manualmente e, além disso, funciona melhor com o borrow checker.

Executar o código da Listagem 8-24 imprimirá {"Yellow": 50, "Blue": 10}. A primeira chamada a entry vai inserir a chave do time Yellow com o valor 50, porque o time Yellow ainda não tem um valor. A segunda chamada a entry não vai alterar o hash map, porque o time Blue já tem o valor 10.

Atualizando um Valor com Base no Valor Antigo

Outro caso de uso comum para hash maps é procurar o valor de uma chave e, em seguida, atualizá-lo com base no valor anterior. Por exemplo, a Listagem 8-25 mostra um código que conta quantas vezes cada palavra aparece em um texto. Usamos um hash map com as palavras como chaves e incrementamos o valor para acompanhar quantas vezes vimos cada palavra. Se for a primeira vez que vemos uma palavra, primeiro inserimos o valor 0.

fn main() {
    use std::collections::HashMap;

    let text = "hello world wonderful world";

    let mut map = HashMap::new();

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

    println!("{map:?}");
}
Listing 8-25: Contando ocorrências de palavras usando um hash map que armazena palavras e contagens

Esse código imprimirá {"world": 2, "hello": 1, "wonderful": 1}. Você pode ver os mesmos pares chave-valor impressos em outra ordem. Lembre-se, pela seção “Acessando Valores em um Hash Map”, que iterar sobre um hash map acontece em ordem arbitrária.

O método split_whitespace retorna um iterador sobre subfatias do valor em text, separadas por espaço em branco. O método or_insert retorna uma referência mutável (&mut V) ao valor da chave especificada. Aqui, armazenamos essa referência mutável na variável count, então, para atribuir um valor a ela, primeiro precisamos desreferenciar count usando o asterisco (*). A referência mutável sai de escopo no fim do loop for, então todas essas mudanças são seguras e permitidas pelas regras de borrowing.

Funções de Hash

Por padrão, HashMap usa uma função de hash chamada SipHash, que pode oferecer resistência a ataques de negação de serviço (DoS) envolvendo tabelas hash1. Esse não é o algoritmo de hash mais rápido disponível, mas o trade-off por maior segurança, mesmo com a queda de desempenho, vale a pena. Se você fizer profiling do seu código e descobrir que a função de hash padrão é lenta demais para o seu caso, pode trocar por outra, especificando um hasher diferente. Um hasher é um tipo que implementa a trait BuildHasher. Falaremos sobre traits e sobre como implementá-las no Capítulo 10. Você não precisa necessariamente implementar seu próprio hasher do zero; crates.io tem bibliotecas compartilhadas por outros usuários de Rust que fornecem hashers com vários algoritmos de hash comuns.

Resumo

Vetores, strings e hash maps oferecem grande parte da funcionalidade necessária em programas quando você precisa armazenar, acessar e modificar dados. Aqui estão alguns exercícios para os quais você já deve estar preparado:

  1. Dada uma lista de inteiros, use um vetor e retorne a mediana, isto é, o valor na posição do meio após ordenar, e a moda, o valor que ocorre com mais frequência. Um hash map será útil aqui.
  2. Converta strings para Pig Latin. A primeira consoante de cada palavra vai para o final da palavra, e adicionamos ay; assim, first vira irst-fay. Palavras que começam com vogal recebem hay no final, então apple vira apple-hay. Tenha em mente os detalhes da codificação UTF-8!
  3. Usando um hash map e vetores, crie uma interface de texto para permitir que a pessoa usuária adicione nomes de funcionários a um departamento de uma empresa; por exemplo, “Add Sally to Engineering” ou “Add Amir to Sales”. Depois, permita que a pessoa usuária recupere uma lista de todas as pessoas de um departamento ou de todas as pessoas da empresa agrupadas por departamento, em ordem alfabética.

A documentação da API da biblioteca padrão descreve métodos de vetores, strings e hash maps que serão úteis para esses exercícios!

Estamos entrando em programas mais complexos, nos quais operações podem falhar, então este é um momento perfeito para discutir tratamento de erros. É o que veremos a seguir!


  1. https://en.wikipedia.org/wiki/SipHash