10. HashMap
📋 Jump to Takeaways🎁 You can store any single type in a Vec. But what if you need to associate keys with values, count occurrences, or check membership in O(1) time?
Key-Value Storage with HashMap
Need to look things up by name instead of position? That's a HashMap: key-value pairs with O(1) average lookups.
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert("Alice", 95);
scores.insert("Bob", 87);
// Access with .get() — returns Option<&V>
if let Some(score) = scores.get("Alice") { // score is &i32 (reference into the map)
println!("Alice scored {}", score); // Alice scored 95
}
// Iteration — name and score are references (&str, &i32)
for (name, score) in &scores {
println!("{}: {}", name, score);
}
}.get() returns Option<&V>, a reference to the value inside the map. The value stays in the map — you're just peeking at it. That's why score above is &i32, not i32. If you want to take the value out (removing it from the map), use .remove():
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::from([("Alice", 95), ("Bob", 87)]);
let peeked = scores.get("Alice"); // Option<&i32> — map still has it
println!("{:?}", peeked); // Some(95)
let taken = scores.remove("Alice"); // Option<i32> — gone from the map
println!("{:?}", taken); // Some(95)
println!("{:?}", scores.get("Alice")); // None — removed
}You must use std::collections::HashMap, it's not in the prelude. Keys must implement Eq and Hash.
If you already know the initial values, use HashMap::from with an array of tuples:
use std::collections::HashMap;
fn main() {
let scores = HashMap::from([
("Alice", 95),
("Bob", 87),
("Carol", 92),
]);
println!("{:?}", scores.get("Bob")); // Some(87)
}There's no literal syntax like Python's {"key": value}. HashMap::from([...]) is the closest equivalent.
The Entry API
Counting things is where the entry API shines. It inserts a value when the key is missing, or lets you tweak the one that's already there, all in one call.
use std::collections::HashMap;
fn main() {
let text = "hello world hello rust hello";
let mut word_count = HashMap::new();
for word in text.split_whitespace() {
let count = word_count.entry(word).or_insert(0);
*count += 1;
}
println!("{:?}", word_count);
// {"hello": 3, "world": 1, "rust": 1} (key order varies between runs)
}.entry(key).or_insert(default) returns a mutable reference to the value. If the key didn't exist, it inserts the default first. The *count += 1 dereferences that reference to modify the actual value in the map — same pattern as the &mut iteration in the previous lesson.
Collecting Iterators into Collections
.collect() is the bridge from iterators back to collections. Run any iterator through it and you get a Vec, a HashMap, or whatever type you ask for. This works because .collect() is generic over any type that implements the FromIterator trait, which is what defines how that type gets built from an iterator. Vec, HashMap, HashSet, and String all implement it. That's why you need to tell .collect() which type you want, it has no way to guess which FromIterator implementation to use.
fn main() {
// Collect a range into a Vec — type annotation tells collect what to build
let numbers: Vec<i32> = (1..=5).collect();
println!("{:?}", numbers); // [1, 2, 3, 4, 5]
// Same thing with "turbofish" syntax — type goes on collect itself
let numbers = (1..=5).collect::<Vec<i32>>();
println!("{:?}", numbers); // [1, 2, 3, 4, 5]
// Filter and collect
let evens: Vec<i32> = (1..=10).filter(|n| n % 2 == 0).collect();
println!("{:?}", evens); // [2, 4, 6, 8, 10]
}Both styles (type annotation on the variable vs turbofish on .collect::< >()) do the same thing — they tell Rust what collection to build.
You can also collect into a HashMap from tuples:
use std::collections::HashMap;
fn main() {
let pairs: HashMap<&str, i32> = vec![("a", 1), ("b", 2)]
.into_iter()
.collect();
println!("{:?}", pairs); // {"a": 1, "b": 2} (key order varies between runs)
}.into_iter() consumes the vec, moving ownership of each element into the iterator. By contrast, .iter() just borrows elements. Here we need into_iter() because HashMap wants to own the key-value pairs, not borrow them.
Bounded Collections
On standard machines Vec grows freely until the OS says no. On embedded systems, IoT devices, or safety-critical targets (rockets, satellites) that's not acceptable — you need a hard cap at compile time.
The heapless crate gives you familiar collection APIs with fixed capacity baked in as a const generic:
use heapless::Vec;
fn main() {
let mut buf: Vec<u8, 256> = Vec::new(); // capacity is 256, fixed forever
match buf.push(42) {
Ok(()) => println!("pushed"),
Err(val) => println!("full, couldn't push {}", val), // no panic, just Err
}
println!("len: {}", buf.len()); // len: 1
}The 256 is resolved at compile time — no heap allocation, no surprise growth. If you try to push beyond capacity you get an Err, not a panic.
The same pattern works for maps with heapless::FnvIndexMap (a fixed-capacity HashMap alternative):
use heapless::FnvIndexMap;
fn main() {
let mut map: FnvIndexMap<&str, i32, 8> = FnvIndexMap::new(); // max 8 entries
map.insert("alice", 95).ok();
map.insert("bob", 87).ok();
println!("{:?}", map.get("alice")); // Some(95)
println!("len: {}", map.len()); // len: 2
}For standard Vec when you're not sure memory is available, try_reserve lets you check before committing:
fn main() {
let mut items: Vec<u32> = Vec::new();
if items.try_reserve(1_000_000).is_err() {
eprintln!("not enough memory");
return;
}
// safe to push up to 1_000_000 items now
}try_reserve returns Err if the allocator can't fulfill the request instead of panicking. Useful when you want to fail gracefully rather than crash.
Key Takeaways
- HashMap provides O(1) key-value lookups, keys must implement
EqandHash - The entry API (
entry(key).or_insert(default)) handles insert-or-update in one call .collect()transforms any iterator into a Vec, HashMap, or other collection that implementsFromIterator.into_iter()consumes and moves;.iter()borrows- Turbofish (
.collect::<Vec<i32>>()) or type annotation both tell.collect()what to build heapless::Vecgives fixed-capacity collections with no heap, ideal for embedded/safety-critical targetstry_reservechecks memory availability before pushing, returningErrinstead of panicking
🎁 You've got Vec and HashMap, but what about guaranteed uniqueness, sorted keys, or a priority queue? Rust's standard library has specialized collections for each of these, and picking the right one makes your code faster and cleaner.