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

8.6 HashMap Pt.2 - Updating HashMaps

8.6.0. Chapter Overview

Chapter 8 is mainly about common collections in Rust. Rust provides many collection-like data structures, and these collections can hold many values. However, the collections covered in Chapter 8 are different from arrays and tuples.

The collections in Chapter 8 are stored on the heap rather than on the stack. That also means their size does not need to be known at compile time; at runtime, they can grow or shrink dynamically.

This chapter focuses on three collections: Vector, String, and HashMap (this article).

8.6.1. Updating a HashMap

A variable-sized HashMap means that the number of key-value pairs can change. However, at any given moment, one key can correspond to only one value. When you want to update data in a HashMap, there are several possible cases:

  • The key you want to update already has a corresponding value in the HashMap:

    • Replace the existing value with a new value
    • Keep the existing value and ignore the new value
    • Merge the existing value with the new value, which means modifying the existing value
  • The key does not exist: add a key-value pair

1. Replacing an Existing Value

If you insert a key-value pair into a HashMap, but the key already exists, the program assigns the new value to that key and overwrites the old one. Example:

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("dev1ce"), 0);
    scores.insert(String::from("dev1ce"), 60);
    println!("{:?}", scores);
}

Here the same key is assigned a value twice: first 0, then 60. The first value is overwritten by the second, which means the final value corresponding to "dev1ce" is 60.

Output:

{"dev1ce": 60}

2. Insert a Value Only if the Key Has No Existing Value

This is the most common case. In this situation, you first need to check whether the original HashMap already contains the key. If it does not, then insert the new value.

Rust provides the entry method to check whether the original HashMap already contains the key. Its argument is the key, and its return value is an Entry enum, which represents whether the value exists. Example:

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("dev1ce"), 0);
    let e = scores.entry(String::from("dev1ce"));
    println!("{:?}", e);
}

This is the case where the key already exists. Output:

Entry(OccupiedEntry { key: "dev1ce", value: 0, .. })

In other words, if the key already exists, the entry method returns an occupied entry (OccupiedEntry) wrapping the existing key-value pair.

Now let’s try the case where the key does not exist. Code:

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("dev1ce"), 0);
    let e = scores.entry(String::from("Zywoo"));
    println!("{:?}", e);
}

Output:

Entry(VacantEntry("Zywoo"))

If the key does not exist, it returns a vacant entry (VacantEntry) associated with the new key.

Now that we can check whether the original HashMap already contains the key, how do we insert or skip insertion based on whether it exists?

Rust provides the or_insert method on Entry, whose argument is the value you want to add. Based on whether the entry is occupied or vacant, it decides whether to insert: if the entry is occupied (the key already exists), it keeps the existing value and does not insert a new one; if the entry is vacant (the key does not exist), it inserts the provided value. Most importantly, it returns a value: a mutable reference to the value for that key. If the key already exists, it returns a mutable reference to the value already in the HashMap; if the key does not exist, it first inserts the key-value pair and then returns a mutable reference to the inserted value. This behavior can be used to build simple counters, as we will see later.

Example:

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("dev1ce"), 0);

    scores.entry(String::from("Zywoo")).or_insert(100);
    scores.entry(String::from("dev1ce")).or_insert(60);
    println!("{:?}", scores);
}
  • The first entry statement looks up "Zywoo". Since it is not found, a vacant entry is returned, and or_insert creates the key-value pair ("Zywoo", 100) using that key and the argument 100.
  • The second entry statement looks up "dev1ce". Since it is found, an occupied entry is returned, and or_insert does not insert a new value, so ("dev1ce", 0) remains unchanged.

Output (key order may vary):

{"Zywoo": 100, "dev1ce": 0}

If this still feels complicated, you can think of scores.entry(String::from("Zywoo")).or_insert(100); as two lines of code:

#![allow(unused)]
fn main() {
let e = scores.entry(String::from("Zywoo"));
e.or_insert(100);
}

3. Updating Based on Existing Values

Let’s start with an example:

use std::collections::HashMap;

fn main() {
    let text = "That's one small step for [a] man, one giant leap for mankind.";

    let mut map = HashMap::new();

    for word in text.split_whitespace() {
        let count = map.entry(word).or_insert(0);
        *count += 1;
    }
    println!("{:#?}", map);
}
  • First, a string literal containing a sentence is declared and assigned to text.
  • Then a HashMap called map is created.
  • Next comes the for loop. text.split_whitespace() splits text into an iterator over strings, and for is used to iterate over it.
  • During iteration, the code checks whether each word appears in the map. If it does, no new value is inserted. If it does not, 0 is inserted as the new value for that key. The key thing to understand is count: because the return value of or_insert is a mutable reference to the value for that key, each time a word appears, the code dereferences the mutable reference and adds 1, which is equivalent to completing one count.

8.6.2. Hash Functions

By default, HashMap uses a cryptographically strong hash function that can resist denial-of-service (DoS) attacks. However, this function is not the fastest hash algorithm available; its advantage is better security. If you think its performance is not good enough, you can also specify a different hasher to switch to another function. A hasher refers to a type that implements the BuildHasher trait.