DEV Community

Cover image for [Rust] 8.6. HashMap Pt.2 - Updating HashMaps
SomeB1oody
SomeB1oody

Posted on

[Rust] 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).

If you find this helpful, please like, bookmark, and follow. To keep learning along, follow this series.

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);  
}
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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);  
}
Enter fullscreen mode Exit fullscreen mode

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

Entry(OccupiedEntry { key: "dev1ce", value: 0, .. })
Enter fullscreen mode Exit fullscreen mode

In other words, if the key already exists, the entry method returns the OccupiedEntry variant of the Entry enum and associates it with 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);  
}
Enter fullscreen mode Exit fullscreen mode

Output:

Entry(VacantEntry("Zywoo"))
Enter fullscreen mode Exit fullscreen mode

If the key does not exist, it returns the VacantEntry variant of the Entry enum and associates it 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, whose argument is the value you want to add. It accepts an Entry enum and uses its two variants to decide whether to insert. If it receives OccupiedEntry (the key already exists), it keeps the existing value and does not insert a new one. If it receives VacantEntry (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);  
}
Enter fullscreen mode Exit fullscreen mode
  • The first entry statement looks up "Zywoo". Since it is not found, VacantEntry is returned, and or_insert receives it and creates the key-value pair ("Zywoo", 100) using the key associated with VacantEntry and the argument 100.
  • The second entry statement looks up "dev1ce". Since it is found, OccupiedEntry is returned, and or_insert receives it and stops the insertion of a new value, so ("dev1ce", 0) remains unchanged.

Output:

{"Zywoo": 100, "dev1ce": 0}
Enter fullscreen mode Exit fullscreen mode

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

let e = scores.entry(String::from("Zywoo"));
e.or_insert(100);
Enter fullscreen mode Exit fullscreen mode

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);  
}
Enter fullscreen mode Exit fullscreen mode
  • 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.

Top comments (0)