A HashMap<K, V> stores key-value pairs and lets you look up a value by its key in (on average) constant time.
Nobody ever got fired for using it for caches, indexes, counters, configuration, and anything else where "given X, find Y" is the question.
Contrary to Vec<T>, HashMap is not in scope by default, so you have to import it first:
use std::collections::HashMap;
let mut config: HashMap<String, String> = HashMap::new();
config.insert("host".to_string(), "localhost".to_string());
let host = config.get("host"); // Option<&String>
Two things to notice:
.get(key) returns Option<&V>, not V.
Missing keys are explicit, no null.
Use .unwrap_or(...) or pattern matching to handle absence.A common pattern is "increment a counter for this key, default to 0":
let mut counts: HashMap<String, u32> = HashMap::new();
for word in ["a", "b", "a"] {
*counts.entry(word.to_string()).or_insert(0) += 1;
}
entry().or_insert() is the idiomatic way to do "look up, or insert a default and then take a mutable reference to it" in one step.
* (dereference)The * in front of counts.entry(...).or_insert(0) is the dereference operator.
or_insert(0) hands back a &mut u32 (a pointer to the value inside the map) and * reaches through that pointer so we can actually update the u32 it points at:
let mut n = 41;
let r: &mut i32 = &mut n;
*r += 1; // updates `n`, not `r`
Without the *, you'd be trying to add 1 to a reference, which the compiler won't let you do.
References were introduced back in the borrowing chapter; for now it's enough to know that when a function returns &mut T, you reach the T through *.
The simplest way to use a HashMap is to create one and insert a few key-value pairs.
HashMap::new() gives you an empty map; the type is usually inferred from the first insert.
Here you'll build a small configuration map with two defaults: a "host" and a "port".
Useful from the standard library
HashMap::newcreates an empty map. The map needs to bemutto insert into it.HashMap::insertadds (or replaces) a key-value pair. Both arguments are owned, so&strliterals need a.to_string()to becomeString.
use std::collections::HashMap;
/// Creates a configuration map with default settings.
/// Returns a `HashMap` with "host" -> "localhost" and "port" -> "8080".
fn create_default_config() -> HashMap<String, String> {
let mut config = HashMap::new();
config.insert("host".to_string(), "localhost".to_string());
config.insert("port".to_string(), "8080".to_string());
config
}
#[test]
fn test_create_default_config() {
let config = create_default_config();
assert_eq!(config.get("host"), Some(&"localhost".to_string()));
assert_eq!(config.get("port"), Some(&"8080".to_string()));
}
Updating a HashMap is the same operation as adding to it: one method covers both cases, and it doesn't care whether the key was already there.
If the key existed, the old value is replaced (and returned); if not, it's inserted fresh.
Useful from the standard library
HashMap::insertis the only call you need here. ReturnsOption<V>:Some(old)if the key was already present,Noneotherwise. You can ignore the return value when you don't care.HashMap::contains_keyis a quick "is this key present?" check that doesn't retrieve the value. Not needed for this exercise, but worth knowing.
use std::collections::HashMap;
/// Updates a configuration value.
/// Inserts or updates the key-value pair.
///
/// One method on `HashMap` covers both cases; it doesn't care whether
/// the key was already there.
fn set_config_value(config: &mut HashMap<String, String>, key: &str, value: &str) {
config.insert(key.to_string(), value.to_string());
}
#[test]
fn test_set_config_value() {
let mut config = HashMap::new();
set_config_value(&mut config, "debug", "true");
assert_eq!(config.get("debug"), Some(&"true".to_string()));
}
Looking up a key in a HashMap returns an Option<&V>, because the key might not be there.
There's no null.
To collapse the Option into a concrete value you supply a fallback for the missing case.
We'll cover Option properly in the Option chapter; here you only need one combinator.
Useful from the standard library
HashMap::getreturnsOption<&V>. The&matters: you get a reference into the map, not a copy of the value.Option::clonedturnsOption<&String>intoOption<String>so you can return an owned value.Option::unwrap_orpulls a value out, falling back to the argument when the option isNone. Reads as "use this value, or default to that one".
use std::collections::HashMap;
/// Gets a configuration value by key.
/// Returns the value if found, "default" if not found.
///
/// Looking up a key returns an `Option<&V>`, because the key might not
/// be there. Collapse it into a concrete value with a fallback for
/// the missing case (`Option` is covered properly in chapter 9).
fn get_config_value(config: &HashMap<String, String>, key: &str) -> String {
match config.get(key) {
Some(value) => value.clone(),
None => "default".to_string(),
}
}
#[test]
fn test_get_config_value() {
let mut config = HashMap::new();
config.insert("timeout".to_string(), "30".to_string());
assert_eq!(get_config_value(&config, "timeout"), "30");
assert_eq!(get_config_value(&config, "missing"), "default");
}
Counting occurrences is the canonical "look up; if missing, insert a default; then update" workflow.
Doing it by hand with contains_key and get_mut works but does two lookups and fights the borrow checker.
The entry API does it in one step.
Useful from the standard library
HashMap::entryreserves the slot for a key in one lookup and hands you back anEntryyou can operate on.Entry::or_insertreturns a&mut V: either to the existing value, or to the default it just inserted. The full pattern reads*map.entry(key).or_insert(0) += 1.- The
*is the dereference operator from the chapter intro: it reaches through the&mut Vso the+= 1updates the count inside the map.
use std::collections::HashMap;
/// Counts how many times each word appears.
/// Returns a `HashMap` mapping words to their counts.
///
/// The naive approach (`if contains_key(k) { *map.get_mut(k).unwrap() += 1 }
/// else { map.insert(k, 1) }`) works, but it does two lookups and fights
/// the borrow checker the moment you try to hold a reference into the map
/// while also calling `insert` on it. The standard idiom is
/// [`Entry::or_insert`](https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html#method.or_insert):
///
/// ```ignore
/// *map.entry(key).or_insert(0) += 1;
/// ```
///
/// `entry(key)` reserves a single "slot" in the map for that key;
/// `or_insert(0)` either returns a mutable reference to the existing value
/// or inserts the default first and returns a reference to that.
/// See: <https://doc.rust-lang.org/std/collections/struct.HashMap.html>
fn count_words(words: &[&str]) -> HashMap<String, usize> {
let mut counts = HashMap::new();
for &word in words {
*counts.entry(word.to_string()).or_insert(0) += 1;
}
counts
}
#[test]
fn test_count_words() {
let words = ["hello", "world", "hello", "rust"];
let counts = count_words(&words);
assert_eq!(counts.get("hello"), Some(&2));
assert_eq!(counts.get("world"), Some(&1));
assert_eq!(counts.get("rust"), Some(&1));
}
You built a configuration map from scratch, updated and read values, and used the entry API to write the canonical word counter in one line.
What we learned
HashMap<K, V>stores key-value pairs and looks them up in (average) constant time. All keys share one type; all values share one type. Mix-and-match goes through enums.HashMaplives instd::collectionsand isn't in the prelude, so you need ause std::collections::HashMap;at the top of the file.insertdoes double duty: it adds a new pair or replaces an existing one, and returns the previous value (if any) asOption<V>.getreturnsOption<&V>, never null. Combine withunwrap_or(orcloned().unwrap_or(...)when you need an owned value) to handle the missing case.entry(key).or_insert(default)is the idiomatic "look up; if missing, insert a default; then return a&mut V" pattern. It does one lookup instead of two and sidesteps the borrow checker.- Reach through a
&mut Twith*to update the value it points at:*map.entry(k).or_insert(0) += 1. References get a proper treatment in the borrowing chapter.