I'll continue Rust's collections discusion that started two articles ago with Vectors and Strings as a special kind of Vectors. In this last dedicated article about collections, I'll bring out the HashMaps! Let's begin.
β οΈ Remember!
You can find all the code snippets for this series in its accompanying repo
If you don't want to install Rust locally, you can play with all the code of this series in the official Rust Playground that can be found on its official page.β οΈβ οΈ The articles in this series are loosely following the contents of "The Rust Programming Language, 2nd Edition" by Steve Klabnik and Carol Nichols in a way that reflects my understanding from a Python developer's perspective.
β I try to publish a new article every week (maybe more if the Rust gods π are generous π) so stay tuned π. I'll be posting "new articles updates" on my LinkedIn and X (fromarly Twitter).
Table of Contents:
What is a Hash Map:
Hash Maps aren't something native to Rust. Other programming languages has similar structures but with different names, like Dictionaries in Python, Objects in Javascrip, Hash Tables in Powershell (althoug not a programming language per-sey), and many more.
They work more or less the same way as you would expect! You retreive a Value with a Key instead of and index as in Vectors.
Rust's HashMap is pretty similar to Python's Dictionary except that in Rust, the HashMap must have the keys of the same type and the values also must be of the same type but the keys and values can be of similar of different types than eachother.
Hash Maps is written in Rust as
HashMap<K, V>
whereK
is the keys' type andV
is the values' type which can be the same.
I'll demonstrate here the basic API of Rust's Hash Maps but as always, the documentations contains a lot of other metods that can be very useful.
Creating a Hash Map:
Appearently Hash Maps isn't used much in Rust that they aren't loaded in the prelude like Vectors and Strings. So, we have to "import" them first wiht the use
keyword then use the new
method to create a new one. In the Example below, I'me creating a map
HashMap with the keys of type String
and the values of type u8
. It should represent the scores that students have obtained after taking and exam. I then inserted the scores of two students, Bob
and Ben
:
use std::collections::HashMap;
fn main() {
// Create a Hash Map
let mut map: HashMap<String, u8> = HashMap::new();
map.insert(String::from("Bob"), 30);
map.insert(String::from("Ben"), 25);
println!("{map:?}");
}
Here, I've used the insert
method to insert the student names (keys) and there exam scores (values). The println
output will be:
{"Bob": 30, "Ben": 25}
Accessing values in a Hash Map:
To access a certain value in a Hash Map, we can use the get
method wich takes the key as its parameter. The get
method retun an Option<&V> which will return Some<&V> in case if the key exists or None if it doesn't. For Example, to get Bob
's score we can use the follwing code:
let bob_score = map.get("Bob").copied().unwrap_or(0);
println!("Bob scored {bob_score}");
OK, this looks a little strange. Let's break it down.
As I've mentioned, the get
method returns the Option<&V> enum of a reference for the values type (here it is &u8
). The copied
method will return Option<V> instead of Option<&V> i.e. Option<u8> as I don't want to return a reference of the value. Then the unwrap_or
method will return V
if the Option enum is Some or a default
value that it takes as a parameter. So, bob_score
will hold eventually the value of the "Bob" key with u8
type and it will be printed as follows:
Bob scored 30
If we haven't used
copied
here, we would write something strange as the paramtere ofunwrap_or
likeunwrap_or(&0)
andbob_scrore
will be a refrence of the retrieved value.
Another way to access values inside a Hash Map is to use a for
loop as follows:
for (key, value) in &map {
println!("{}: {}", key, value);
}
This will print the following as expected:
Ben: 25
Bob: 30
One more thing before we can leave this section is that Mash Maps takes ownership over the passed keys and values. Look at the follwing example:
let jane = String::from("Jane");
let jane_score = 29;
map.insert(jane, jane_score);
println!("{jane} scored {jane_score}"); // Error: borrow of moved value: `jane`
Here we have declared jane
as String and her score jane_score
as i32. As we have used the insert
method, jane
has "moved" into the Hash Map as it's a String while jane_score
is "copied" as it's a simple type. The println
will cause a compilation error as jane
isn't valid anymore after the usage of insert
.
Updating a Hash Map:
We usualy update a Hash Map in one of the following scenarios:
- Overwite the old values
- Only update if the key isn't present
- Update a value based on its old value Let's see how we can perform each.
Overwrite the values:
This is probably the simplest implementation. We just have to use insert
using a pre-existing key.
map.insert(String::from("Jane", 29);
let jane_score = map.get("Jane").copied().unwrap();
println!("Jane score before the update {jane_score}");
map.insert(String::from("Jane"), 30);
let jane_score = map.get("Jane").copied().unwrap();
println!("Jane score after the update {jane_score}");
Here, we have updated Jane
's score from 29
to 30
Jane score before the update 29
Jane score after the update 30
Only update if the key isn't present:
In this use case, we use the entry
method that takes a key as a parameter and returns Entry enum representing an entry that could exist or not. Then we use a similarly useful method, or_insert
that insert the passed value if the entry doesn't exist. Let's look at this example:
map.entry(String::from("Jane")).or_insert(25);
map.entry(String::from("Windy")).or_insert(25);
println!("{map:?}");
Here, we were retrieving the Jane
and Windy
entries and inserting 25
as their exam score if they arent present. As Jane
entry already exist from the last section, it will remain unchanged with the value of 30
. As for Windy
, because it's a new key, it will be inserted with the value of 25
and the println
will print the following:
{"Ben": 25, "Windy": 25, "Bob": 30, "Jane": 30}
Update a value based on its old value:
Let's say that we will give all students a 5 points bonus. So, we will have a new value of a certain key that is the result of the addition of its old value and the 5 points bonus. One way to do this is as follows:
for (_, score) in map.iter_mut() {
*score += 5;
}
println!("{map:?}");
Let's explain what's going on here.
First, I've used the iter_mut
method that returns "mutable" references of the keys and values of the Hash Map because I want to change the values. And as I don't care about the keys, I've used the underscore in its place in the for
expression. Now, score
is a &mut u8
or a mutable reference of the values of u8
type. Next, I've used the "de-referencing" operator "*" on score
. We will have a look at de-referencing later, but let's just say it's a way to get the value that the reference is "referencing".
If I didn't use the "*" here, the compilier will complain about not being able to add {integer} refering to the
5
and&mut u8
(thescore
) as it considers them of different types.
Then I've added the 5 points bonus to the old values.
The result of this will be all the values have increase by 5 from their old values (compare them to the last println output):
{"Ben": 30, "Windy": 30, "Bob": 35, "Jane": 35}
We have now looked at Hash Maps and saw how they are almost similar to Python's Dictionaries except for the types restrictions.
Next we will look at Rust's Error Handling. See you then π
Top comments (0)