A HashMap is a collection of key-value pairs designed to look up data by its key.
If we struggled a bit with Strings in the previous article, today we return to familiar territory. Nearly every language has a data structure for storing key-value pairs.
- In Python:
dict - In JavaScript:
ObjectorMap - In C#:
Dictionary<K, V> - In C++:
std::unordered_map
In Rust, this structure is called HashMap. It’s the perfect tool when you want to look up data not by a numeric index (as in a Vector), but by a label, a name, or any other unique key.
Creating a HashMap
The first thing you should know is that HashMap is a bit less “famous” than Vec or String. It is not included in the prelude (the tools Rust loads automatically).
To use it, we must explicitly import it from the standard library:
use std::collections::HashMap; // <--- Essential
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Red"), 50);
}Like vectors, HashMap dynamically allocates storage for its entries and is homogeneous: all keys must be of the same type, and all values must be of the same type.
Accessing Values
To retrieve a piece of data, we use the .get() method, passing a reference to the key.
let team = String::from("Blue");
let points = scores.get(&team); // Returns Option<&i32>Here, Rust returns an Option<&i32>. Why?
- Option: Because the key might not exist (it returns
None). - &i32 (Reference): Because the HashMap still owns the data. It is only lending it to us for reading.
We can use if let or match to handle the result:
if let Some(points) = scores.get(&team) {
println!("The team has {} points", points);
} else {
println!("The team does not exist");
}HashMap and Ownership
At this point, Rust behaves differently than C# or Java. In those languages, if you use an object as a key, it simply stores a reference.
In Rust, insert takes the key and value by value, and the HashMap becomes the owner of what it stores. If an argument implements Copy, a copy is stored and the original variable remains valid; otherwise, it is moved.
Observe what happens here:
use std::collections::HashMap;
fn main() {
let key = String::from("Favorite Color");
let value = String::from("Blue");
let mut map = HashMap::new();
// When inserting, the variables are MOVED into the map
map.insert(key, value);
// println!("{}", key); // ❌ ERROR! 'key' is no longer valid.
// println!("{}", value); // ❌ ERROR! 'value' is no longer valid.
}The variables key and value have been moved into the map. The map is now responsible for freeing them when it is itself destroyed.
If you want to keep using the key after inserting it, you must clone it when inserting: map.insert(key.clone(), value).
If the key were a simple type (i32), it would be copied automatically and there would be no problem.
Updating a HashMap
Modifying data in a map is a very common operation, and Rust has a very elegant API called the Entry API to handle cases of “if it exists, update; if not, create.”
Overwriting a Value
If you insert a key that already exists, the old value is replaced. insert returns that value wrapped in an Option<V>; if we ignore the result, it will be dropped.
map.insert(String::from("Blue"), 10);
map.insert(String::from("Blue"), 25); // The 10 is lost; now it's 25Insert Only If the Key Is Vacant
Sometimes we want to set a default value only if the key is empty.
map.entry(String::from("Yellow")).or_insert(50);
map.entry(String::from("Blue")).or_insert(50); // Does nothing, "Blue" is already 25The .entry() method returns a special enum representing the slot where the value could be. .or_insert() inserts the value only if that slot is vacant and, most importantly, returns a mutable reference to the value (whether new or old).
Updating Based on the Previous Value
Imagine we want to count how many times each word appears in a text.
let text = "hello wonderful world world";
let mut word_count = HashMap::new();
for word in text.split_whitespace() {
// 1. entry looks up the key
// 2. or_insert(0) ensures a value exists (starts at 0 if new)
// 3. Returns a mutable reference (&mut i32) to the value
let count = word_count.entry(word).or_insert(0);
// 4. Dereference and add 1
*count += 1;
}
println!("{:?}", word_count);
// {"world": 2, "hello": 1, "wonderful": 1}The entry API allows performing the lookup and possible insertion without manually repeating both operations.
The Hashing Algorithm
The standard library’s HashMap uses a randomly seeded hasher by default to provide resistance against HashDoS attacks. The specific algorithm is not part of the stable API and may change between versions.
For the vast majority of applications, the default is a reasonable choice. If you have measured that hashing is a bottleneck, you can provide a different BuildHasher implementation, while also considering its security properties.