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 show up properly in chapter 11; 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.
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.
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 chapter 9; 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".
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.
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 chapter 11.