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 tipoV. Ele faz isso através de um hashing function, que determina como ele coloca essas chaves e valores em memória. Muitas linguagens de programação diferentes suportam este tipo de estrutura de dados, mas muitas vezes com um nome diferente: hash, map, object, hash table ou associative array, apenas para citar alguns.

Os Hash maps são úteis para quando você deseja poder procurar dados sem uso de índice, como você pode com vetores, mas usando uma chave que pode ser de qualquer tipo. Por exemplo, em um jogo, você poderia acompanhar a pontuação de cada equipe em um hash map onde cada chave é o nome de uma equipe e os valores são cada pontuação da equipe. Dado um nome da equipe, você pode recuperar sua pontuação.

Examinaremos a API básica dos hash map neste capítulo, mas há muitos mais coisas escondidas nas funções definidas no HashMap pela biblioteca padrão. Como sempre, verifique a documentação da biblioteca padrão para mais informação.

Criando um novo Hash Map

Podemos criar um HashMap vazio com new, e adicionar elementos com insert. Aqui, estamos acompanhando as pontuações de duas equipes cujos nomes são Blue e Yellow. A equipe blue começará com 10 pontos e a equipe yellow começa com 50:


# #![allow(unused_variables)]
#fn main() {
use std::collections::HashMap;

let mut scores = HashMap::new();

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

Observe que precisamos primeiro use o HashMap da parte de coleções da biblioteca padrão. De nossas três coleções comuns, esta é a de menor frequencia de uso, por isso não está inclusa nos recursos importados automaticamente no prelúdio. Os Hash maps também têm menos suporte da biblioteca padrão; não há macro embutida para construí-los, por exemplo.

Assim como os vetores, os mapas hash armazenam seus dados no heap. Este HashMap tem chaves do tipo String e valores do tipo i32. Como vetores, os hash maps são homogêneos: todas as chaves devem ter o mesmo tipo e todos os valores devem ter o mesmo tipo.

Outra maneira de construir um hash map é usando o método collect em um vetor de tuplas, onde cada tupla consiste de uma chave e seu valor. O método collect reúne dados em vários tipos de coleção, incluindo HashMap. Por exemplo, se tivéssemos os nomes das equipes e as pontuações iniciais em dois vetores separados, podemos usar o método zip para criar um vetor de tuplas onde “Blue” é emparelhado com 10, e assim por diante. Então podemos usar o método collect para transformar esse vetor de tuplas em um HashMap:


# #![allow(unused_variables)]
#fn main() {
use std::collections::HashMap;

let teams  = vec![String::from("Blue"), String::from("Yellow")];
let initial_scores = vec![10, 50];

let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
#}

A anotação de tipo HashMap <_, _> é necessária aqui porque é possível collect em muitas estruturas de dados diferentes, e Rust não sabe qual você deseja, a menos que você especifique. Para os parâmetros de tipo, para os tipos de chave e valor, no entanto, usamos underscores e Rust pode inferir os tipos que o hash map contém com base nos tipos de dados no vetor.

Hash Maps e Ownership

Para os tipos que implementam a Copy trait, como i32, os valores são copiados no hash map. Para valores owned como String, os valores serão movidos e o hash map será o owner desses valores:


# #![allow(unused_variables)]
#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 e field_value são inválidos neste ponto
#}

Não poderíamos usar as ligações field_name efield_value depois que foram transferidos para o hash map com a chamada para insert.

Se inserimos referências a valores no hash map, os próprios valores não serão movido para o hash map. Os valores que as referências apontam devem ser válido pelo menos enquanto o hash map seja válido, no entanto. Falaremos mais sobre esses problemas na seção Lifetimes do Capítulo 10.

Acessando Valores em um Hash Map

Podemos obter um valor do hash map fornecendo a chave para o método get:


# #![allow(unused_variables)]
#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);
#}

Aqui, score terá o valor que está associado à equipe Blue, e o resultado será Some(&10). O resultado está envolvido em Some porque get retorna Option<&V>; se não houver valor para essa chave no hash map, get retornará None. O programa precisará lidar com Option em uma das formas que abordamos no Capítulo 6.

Podemos iterar sobre cada par chave/valor em um hash map de uma maneira similar à que fazemos com vetores, usando um loop for:


# #![allow(unused_variables)]
#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);
}
#}

Isso imprimirá cada par, em uma ordem arbitrária:

Yellow: 50
Blue: 10

Atualizando um Hash Map

Embora o número de chaves e valores sejam crescentes, cada chave individual pode apenas tem um valor associado a ele por vez. Quando queremos mudar os dados em um hash map, temos que decidir como lidar com o caso quando uma chave já possui uma valor atribuído. Poderíamos optar por substituir o valor antigo pelo novo valor, desconsiderando completamente o valor antigo. Poderíamos escolher manter o valor antigo e ignorar o novo valor, e apenas adicione o novo valor se a chave ainda não tem um valor. Ou podemos combinar o valor antigo ao valor novo. Vejamos como fazer cada um desses!

Sobrescrevendo um Valor

Se inserimos uma chave e um valor em um hash map, então se inserir essa mesma chave com um valor diferente, o valor associado a essa chave será substituído. Eembora o seguinte código chame insert duas vezes, o hash map só conterá um par de chave/valor porque inserimos o valor da chave da equipe Blue ambas as vezes:


# #![allow(unused_variables)]
#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);
#}

Isso imprimirá {"Blue": 25}. O valor original de 10 foi substituído.

Insira Apenas se a Chave Não Possui Valor

É comum querer verificar se uma determinada chave tem um valor e, se não tiver, inserir um valor para ela. Os Hash maps possuem uma API especial para isso, chamada entry, que leva a chave que queremos verificar como um argumento. O valor de retorno da função entry é um enum, Entry, que representa um valor que pode ou não existir. Digamos que queremos verificar se a chave para o time Yellow tem um valor associado a ela. Se não tiver, queremos inserir o valor 50, e o mesmo para a equipe Blue. Com a API de entrada, o código irá parecer com:


# #![allow(unused_variables)]
#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);
#}

O método or_insert em Entry retorna o valor para o Entry correspondente se a chave existir, e se não, insere seu argumento como o novo valor para esta chave e retorna a Entry modificada. Isso é muito mais limpo do que escrever a lógica por nós mesmos e, além disso, trabalha-se de forma mais limpa com o borrow checker.

Este código imprimirá {"Yellow": 50, "Blue": 10}. A primeira chamada para entry irá inserir a chave para a equipe Yellow com o valor 50, uma vez que o time Yellow já não possua um valor. A segunda chamada para entry não vai mudar o hash map pois o time Blue já possui o valor 10.

Atualize um Valor com Base no Valor Antigo

Outro caso de uso comum para hash maps é procurar o valor de uma chave e, em seguida, atualiza-la , com base no valor antigo. Por exemplo, se quisermos contar quantas vezes cada palavra apareceu em algum texto, podemos usar um hash map com as palavras como chaves e incrementar o valor para acompanhar quantas vezes vimos essa palavra. Se esta é a primeira vez que vimos uma palavra, primeiro inseriremos o valor 0.


# #![allow(unused_variables)]
#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);
#}

Isso imprimirá {"world": 2, "hello": 1, "wonderful": 1}. O método or_insert na verdade retorna uma referência mutável (& mutV) para o valor desta chave. Aqui nós armazenamos essa referência mutável na variável count, então, para poder atribuir esse valor, devemos primeiro desreferenciar count usando o asterisco (*). A referência mutável fica fora do escopo no final do loop for, então todas essas mudanças são seguras e permitidas pelas regras de borrow.

Funções Hashing

Por padrão, HashMap usa uma função de hashing criptográficamente segura que pode fornecer resistência aos ataques de Negação de Serviço (DoS). Este não é o algoritmo mais rápido de hashing por aí, mas a compensação por uma melhor segurança que vem com a queda na performance vale a pena. Se você testar a velocidade do seu código e encontrar que a função de hash padrão é muito lenta para seus propósitos, você pode mudar para outra função especificando um hasher diferente. Um hasher é um tipo que implementa a trait BuildHasher. Vamos falar sobre traits e como implementá-los no Capítulo 10. Você não precisa necessariamente implementar o seu próprio hasher do zero; crates.io tem bibliotecas de hashers de uso comum que outras pessoas compartilharam lá.

Sumário

Vetores, strings e hash maps irão levá-lo longe em programas onde você precisa armazenar, acessar e modificar dados. Aqui estão alguns exercícios que você deve estar capacitado para resolver:

  • Dada uma lista de inteiros, use um vetor e retorne a média, a mediana    (quando classificado, o valor na posição do meio) e modo (o valor que    ocorre com mais frequência; um hash map será útil aqui) da lista.
  • Converta strings para Pig Latin, onde a primeira consoante de cada palavra é movida    para o final da palavra adicionado um "ay" , então “first” se torna “irst-fay”.    Palavras que começam com uma vogal recebem “hay” adicionado ao final (“apple”    torna-se “apple-hay”). Lembre-se sobre a codificação UTF-8!
  • Usando um hash map e vetores, crie uma interface de texto para permitir que um usuário adicione    nomes de funcionários para um departamento da empresa. Por exemplo, “Add Sally to Engineering” ou “Add Amir to Sales”. Em seguida, deixe o usuário recuperar uma lista de todas    as pessoas de um departamento ou todas as pessoas na empresa por departamento, ordenadas    alfabeticamente.

A documentação da API da biblioteca padrão descreve métodos que esses tipos possuem que será útil para esses exercícios!

Estamos entrando em programas mais complexos onde as operações podem falhar, o que significa que é um momento perfeito para passar pelo tratamento de erros em seguida!