Two hashmaps broke up.
They didn’t share the same values.
A HashMap<K, V> stores key-value pairs and lets you look up a value by its key
in (on average) constant time. It's useful for caches, indexes, counters, and
configuration.
Unlike 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>
The type annotation says that every key in this map is a String, and so is
every value. If you need several possible value types in one map, an enum can
represent those choices.
The .get(key) call returns Option<&V>, not V. A missing key becomes None
instead of a null value, so you handle the absence with .unwrap_or(...) or
pattern matching.
Use entry when a default should fill a gap without replacing an existing
value:
config.entry("theme".to_string()).or_insert("light".to_string());
Unlike insert, this leaves an existing theme unchanged. or_insert returns a
mutable reference to the value, whether it was already there or just inserted.
You can use that reference to change the stored value without another lookup.
* (Dereference)The dereference operator, *, lets you change a value through a mutable
reference. This works with references returned by or_insert, just as it does
with a reference to a local number:
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. You met references in the
borrowing chapter. Here, the practical rule
is 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. This map storesStringkeys and values, so convert the&strliterals with.to_string().
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. It returnsOption<V>:Some(old)if the key was already present,Noneotherwise. You can ignore the return value when you don't care.
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()));
set_config_value(&mut config, "theme", "dark");
set_config_value(&mut config, "debug", "false");
assert_eq!(config.get("debug"), Some(&"false".to_string()));
assert_eq!(config.get("theme"), Some(&"dark".to_string()));
assert_eq!(config.len(), 2);
}
Looking up a key in a HashMap returns an Option<&V>, because the key might
not be there. There's no null. To get a value out of the Option, you supply a
fallback for the missing case. We'll cover Option in its own chapter. For now,
you can clone the value if present and use unwrap_or to supply a fallback.
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. You can read it 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");
}
Count how often each word occurs. Unlike the configuration exercises, repeated
keys shouldn't replace a value: every occurrence contributes to the same count.
Use the entry API to handle both new and repeated words.
Useful from the Standard Library
HashMap::entrylooks up a key and returns anEntryrepresenting either an occupied or a vacant entry.Entry::or_insertreturns a&mut V: either to the existing value, or to the default it just inserted.- The
*is the dereference operator from the chapter intro: it reaches through the&mut Vto let you change the value inside the map.
use std::collections::HashMap;
/// Counts how many times each word appears. Returns a `HashMap` mapping words
/// to their counts.
///
/// Use the `entry` API to work with new and repeated words through the same
/// lookup. Its mutable reference lets you update the count inside the map.
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 counting step of a 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. If you need different value types in one map, put them in an enum.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, without needing to callinsertwhile holding a reference into the map.- Reach through a
&mut Twith*to update the value it points at:*map.entry(k).or_insert(0) += 1. This is the same dereference operation you met in the borrowing chapter.
Extra practice to explore at your own pace. These chapters do not count toward course progress.