Lesson 42 of 94
HashMap
HashMap<K, V> maps keys to values with average O(1) lookups. You met the basics in A First Look at String, Vec & HashMap; this lesson covers it in depth. The API will feel familiar, with two Rust-specific themes:
- Ownership: inserting moves the key and value into the map. Reading gives you references.
- The entry API: one lookup to "get or insert, then update", a single, borrow-checker-friendly tool.
HashMap lives in std::collections, so it needs a use line.
| Operation | Rust |
|---|---|
| Insert | map.insert(k, v) (returns the old value as Option) |
| Look up | map.get(&k) returns Option<&V> |
| Look up with a default | map.get(&k).copied().unwrap_or(d) |
| Check for a key | map.contains_key(&k) |
| Remove | map.remove(&k) returns Option<V> |
| Insert lazily if missing | map.entry(k).or_insert_with(f) |
| Count occurrences | *map.entry(k).or_insert(0) += 1 |
| Iterate over entries | map.iter() |
| Build from literals | HashMap::from([("a", 1)]) |
| Sorted map | BTreeMap: BTreeMap & BTreeSet |
1. Creating a map and inserting
use std::collections::HashMap;
fn main() {
let mut prices: HashMap<String, u32> = HashMap::new();
prices.insert(String::from("apple"), 120);
prices.insert(String::from("pear"), 95);
prices.insert(String::from("kiwi"), 60);
println!("{} products", prices.len());
println!("pear costs {:?}", prices.get("pear"));
}3 products
pear costs Some(95)2. get returns an Option
A missing key is None, never null, so you can't forget to handle it.
use std::collections::HashMap;
fn main() {
let stock = HashMap::from([("apple", 3), ("pear", 0)]);
for item in ["apple", "pear", "mango"] {
match stock.get(item) {
Some(0) => println!("{item}: sold out"),
Some(n) => println!("{item}: {n} left"),
None => println!("{item}: not sold here"),
}
}
let mango = stock.get("mango").copied().unwrap_or(0);
println!("mango count with default: {mango}");
}apple: 3 left
pear: sold out
mango: not sold here
mango count with default: 0HashMap::from([...]) builds a map from an array of pairs. .copied() turns the Option<&i32> into an Option<i32>.
3. Indexing with map[key] panics on missing keys
map["key"] is convenient when a missing key would be a bug. For normal lookups, use get.
use std::collections::HashMap;
fn main() {
let config = HashMap::from([("host", "localhost"), ("port", "8080")]);
println!("{}:{}", config["host"], config["port"]);
println!("{}", config["timeout"]);
}localhost:8080
thread 'main' panicked at src/main.rs:6:26:
no entry found for key4. Inserting moves owned keys and values
A String key is moved into the map. This is the ownership rule from Ownership, not a special map rule.
use std::collections::HashMap;
fn main() {
let mut sessions = HashMap::new();
let user = String::from("ana");
sessions.insert(user, 42);
println!("inserted session for {user}");
}error[E0382]: borrow of moved value: `user`
--> src/main.rs:7:37
|
5 | let user = String::from("ana");
| ---- move occurs because `user` has type `String`, which does not implement the `Copy` trait
6 | sessions.insert(user, 42);
| ---- value moved here
7 | println!("inserted session for {user}");
| ^^^^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
6 | sessions.insert(user.clone(), 42);
| ++++++++Insert a clone if you still need the value, or use the key before inserting it. Keys of Copy types, like integers, are simply copied.
5. Looking up String keys with &str
A HashMap<String, V> can be queried with a &str, no allocation needed. This works through the Borrow trait: a String can be borrowed as a str, and get accepts any borrowed form of the key type.
use std::collections::HashMap;
fn main() {
let mut users: HashMap<String, u32> = HashMap::new();
users.insert(String::from("ana"), 34);
let name = "ana";
let owned = String::from("ana");
println!("{:?} {:?} {:?}", users.get(name), users.get(&owned), users.get("zed"));
}Some(34) Some(34) None6. insert returns the previous value
Inserting an existing key replaces the value and hands back the old one.
use std::collections::HashMap;
fn main() {
let mut latest_version = HashMap::new();
println!("{:?}", latest_version.insert("serde", "1.0.200"));
println!("{:?}", latest_version.insert("serde", "1.0.210"));
println!("{:?}", latest_version.get("serde"));
}None
Some("1.0.200")
Some("1.0.210")7. contains_key, remove, and remove_entry
use std::collections::HashMap;
fn main() {
let mut carts = HashMap::from([(101, 3), (102, 1), (103, 7)]);
println!("{}", carts.contains_key(&102));
println!("{:?}", carts.remove(&102));
println!("{:?}", carts.remove(&999));
println!("{:?}", carts.remove_entry(&103));
println!("{} carts left", carts.len());
}true
Some(1)
None
Some((103, 7))
1 carts left8. Iteration order is not defined
Iteration order is effectively random, and Rust even randomizes it per program run to resist hash-flooding attacks. For stable output, sort the entries, or use a BTreeMap.
use std::collections::HashMap;
fn main() {
let scores = HashMap::from([("ben", 71), ("ana", 92), ("cy", 85)]);
let mut entries: Vec<(&&str, &i32)> = scores.iter().collect();
entries.sort();
for (name, score) in entries {
println!("{name}: {score}");
}
}ana: 92
ben: 71
cy: 85Every other example in this lesson sorts before printing, so the output is the same on every run.
9. keys, values, and values_mut
use std::collections::HashMap;
fn main() {
let mut balances = HashMap::from([("ana", 100), ("ben", 250), ("cy", 40)]);
let total: i32 = balances.values().sum();
let mut names: Vec<&str> = balances.keys().copied().collect();
names.sort();
println!("{:?} total {total}", names);
for balance in balances.values_mut() {
*balance += 10; // monthly bonus
}
let mut after: Vec<(&str, i32)> = balances.into_iter().collect();
after.sort();
println!("{:?}", after);
}["ana", "ben", "cy"] total 390
[("ana", 110), ("ben", 260), ("cy", 50)]10. get_mut: change a value in place
use std::collections::HashMap;
fn main() {
let mut inventory = HashMap::from([("widget", 10), ("gadget", 3)]);
if let Some(qty) = inventory.get_mut("widget") {
*qty -= 4;
}
match inventory.get_mut("gizmo") {
Some(qty) => *qty -= 1,
None => println!("gizmo not in inventory"),
}
println!("{:?}", inventory.get("widget"));
}gizmo not in inventory
Some(6)11. The entry API: count occurrences
entry(key) looks the key up once and returns a handle to that slot. or_insert(v) inserts v if the slot is empty and returns a &mut to the value either way.
use std::collections::HashMap;
fn main() {
let text = "the cat and the hat and the bat";
let mut counts: HashMap<&str, u32> = HashMap::new();
for word in text.split_whitespace() {
*counts.entry(word).or_insert(0) += 1;
}
let mut sorted: Vec<(&str, u32)> = counts.into_iter().collect();
sorted.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
println!("{:?}", sorted);
}[("the", 3), ("and", 2), ("bat", 1), ("cat", 1), ("hat", 1)]12. or_insert_with and or_default
or_insert_with(f) only runs f when the key is missing. or_default() inserts the type's default value (0, empty Vec, ...).
use std::collections::HashMap;
fn expensive_profile(user: &str) -> String {
println!(" loading profile for {user}");
format!("{user}-profile")
}
fn main() {
let mut cache: HashMap<&str, String> = HashMap::new();
for user in ["ana", "ben", "ana", "ana"] {
let profile = cache.entry(user).or_insert_with(|| expensive_profile(user));
println!("{profile}");
}
let mut totals: HashMap<&str, u64> = HashMap::new();
for (region, amount) in [("eu", 10), ("us", 5), ("eu", 7)] {
*totals.entry(region).or_default() += amount;
}
println!("eu={} us={}", totals["eu"], totals["us"]);
} loading profile for ana
ana-profile
loading profile for ben
ben-profile
ana-profile
ana-profile
eu=17 us=513. and_modify(...).or_insert(...)
Update the value if present, otherwise insert a starting value, in one lookup.
use std::collections::HashMap;
fn main() {
let mut last_login: HashMap<&str, (u64, u32)> = HashMap::new(); // (timestamp, login count)
for (user, time) in [("ana", 100), ("ben", 150), ("ana", 300)] {
last_login
.entry(user)
.and_modify(|(t, count)| {
*t = time;
*count += 1;
})
.or_insert((time, 1));
}
let mut v: Vec<_> = last_login.into_iter().collect();
v.sort();
println!("{:?}", v);
}[("ana", (300, 2)), ("ben", (150, 1))]14. Matching on Entry::Occupied and Entry::Vacant
For full control, match on the entry itself.
use std::collections::HashMap;
use std::collections::hash_map::Entry;
fn main() {
let mut usernames: HashMap<String, u32> = HashMap::new();
for (name, id) in [("ferris", 1), ("gopher", 2), ("ferris", 3)] {
match usernames.entry(name.to_string()) {
Entry::Vacant(slot) => {
slot.insert(id);
println!("registered {name} as user {id}");
}
Entry::Occupied(existing) => {
println!("'{name}' is taken by user {}", existing.get());
}
}
}
}registered ferris as user 1
registered gopher as user 2
'ferris' is taken by user 115. Why the entry API exists
The "check, then insert" pattern fights the borrow checker: the reference returned by get is still alive when you try to insert.
use std::collections::HashMap;
fn get_or_create<'a>(map: &'a mut HashMap<String, Vec<u32>>, key: &str) -> &'a mut Vec<u32> {
if let Some(v) = map.get_mut(key) {
return v;
}
map.insert(key.to_string(), Vec::new());
map.get_mut(key).unwrap()
}
fn main() {}error[E0499]: cannot borrow `*map` as mutable more than once at a time
--> src/main.rs:7:5
|
3 | fn get_or_create<'a>(map: &'a mut HashMap<String, Vec<u32>>, key: &str) -> &'a mut Vec<u32> {
| -- lifetime `'a` defined here
4 | if let Some(v) = map.get_mut(key) {
| --- first mutable borrow occurs here
5 | return v;
| - returning this value requires that `*map` is borrowed for `'a`
6 | }
7 | map.insert(key.to_string(), Vec::new());
| ^^^ second mutable borrow occurs hereThis is the loop-return limitation from Fixing Borrow Checker Errors. The entry API does it in one lookup, and compiles:
use std::collections::HashMap;
fn get_or_create<'a>(map: &'a mut HashMap<String, Vec<u32>>, key: &str) -> &'a mut Vec<u32> {
map.entry(key.to_string()).or_default()
}
fn main() {
let mut orders: HashMap<String, Vec<u32>> = HashMap::new();
get_or_create(&mut orders, "ana").push(1001);
get_or_create(&mut orders, "ana").push(1002);
println!("{:?}", orders.get("ana"));
}Some([1001, 1002])16. Grouping values: HashMap<K, Vec<V>>
use std::collections::HashMap;
fn main() {
let orders = [("ana", 30), ("ben", 12), ("ana", 7), ("cy", 50), ("ben", 3), ("ana", 9)];
let mut by_customer: HashMap<&str, Vec<u32>> = HashMap::new();
for (customer, amount) in orders {
by_customer.entry(customer).or_default().push(amount);
}
let mut rows: Vec<(&str, Vec<u32>)> = by_customer.into_iter().collect();
rows.sort();
for (customer, amounts) in rows {
let total: u32 = amounts.iter().sum();
println!("{customer}: {:?} total {total}", amounts);
}
}ana: [30, 7, 9] total 46
ben: [12, 3] total 15
cy: [50] total 5017. Building a map with collect
An iterator of pairs collects into a HashMap. Later duplicates overwrite earlier ones.
use std::collections::HashMap;
fn main() {
let users = [(1, "ana"), (2, "ben"), (3, "cy")];
let by_id: HashMap<u32, &str> = users.iter().copied().collect();
let name_lengths: HashMap<&str, usize> = users.iter().map(|&(_, n)| (n, n.len())).collect();
println!("{:?} {:?}", by_id.get(&2), name_lengths.get("cy"));
let with_duplicates: HashMap<&str, i32> = [("x", 1), ("x", 2)].into_iter().collect();
println!("{:?}", with_duplicates);
}Some("ben") Some(2)
{"x": 2}18. Custom struct keys
Any type implementing Eq and Hash can be a key. Derive them, as in Deriving Standard Traits.
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct Cell {
row: i32,
col: i32,
}
fn main() {
let mut board: HashMap<Cell, char> = HashMap::new();
board.insert(Cell { row: 0, col: 0 }, 'X');
board.insert(Cell { row: 1, col: 1 }, 'O');
board.insert(Cell { row: 0, col: 2 }, 'X');
println!("{:?}", board.get(&Cell { row: 1, col: 1 }));
println!("{:?}", board.get(&Cell { row: 2, col: 2 }));
println!("{} pieces placed", board.len());
}Some('O')
None
3 pieces placed19. Floats can't be keys
f64 is not Eq (because of NaN) and not Hash, so it can't be a key. Store an integer representation, such as cents or a scaled value.
use std::collections::HashMap;
fn main() {
let mut by_price: HashMap<f64, &str> = HashMap::new();
by_price.insert(9.99, "book");
}error[E0599]: the method `insert` exists for struct `HashMap<f64, &str>`, but its trait bounds were not satisfied
--> src/main.rs:5:14
|
5 | by_price.insert(9.99, "book");
| ^^^^^^
|
= note: the following trait bounds were not satisfied:
`f64: Eq`
`f64: Hash`20. retain: remove entries matching a condition
use std::collections::HashMap;
fn main() {
let mut sessions = HashMap::from([("s1", 100), ("s2", 950), ("s3", 400), ("s4", 1_200)]);
let now = 1_000;
let ttl = 500;
sessions.retain(|_, &mut last_seen| now - last_seen.min(now) < ttl);
let mut alive: Vec<&str> = sessions.keys().copied().collect();
alive.sort();
println!("{:?}", alive);
}["s2", "s4"]21. Merging two maps
extend inserts every pair from another map, overwriting existing keys. For combining values, use the entry API.
use std::collections::HashMap;
fn main() {
let defaults = HashMap::from([("theme", "light"), ("lang", "en"), ("tz", "UTC")]);
let user_prefs = HashMap::from([("theme", "dark"), ("tz", "Europe/Lisbon")]);
let mut effective = defaults.clone();
effective.extend(user_prefs);
let mut v: Vec<_> = effective.into_iter().collect();
v.sort();
println!("{:?}", v);
let q1 = HashMap::from([("eu", 100), ("us", 80)]);
let q2 = HashMap::from([("us", 50), ("apac", 30)]);
let mut year = q1.clone();
for (region, sales) in q2 {
*year.entry(region).or_insert(0) += sales;
}
let mut v: Vec<_> = year.into_iter().collect();
v.sort();
println!("{:?}", v);
}[("lang", "en"), ("theme", "dark"), ("tz", "Europe/Lisbon")]
[("apac", 30), ("eu", 100), ("us", 130)]22. Inverting a map
use std::collections::HashMap;
fn main() {
let country_of = HashMap::from([("Lisbon", "PT"), ("Porto", "PT"), ("Berlin", "DE"), ("Munich", "DE"), ("Paris", "FR")]);
let mut cities_in: HashMap<&str, Vec<&str>> = HashMap::new();
for (&city, &country) in &country_of {
cities_in.entry(country).or_default().push(city);
}
let mut v: Vec<(&str, Vec<&str>)> = cities_in.into_iter().collect();
v.sort();
for (country, cities) in &mut v {
cities.sort();
println!("{country}: {:?}", cities);
}
}DE: ["Berlin", "Munich"]
FR: ["Paris"]
PT: ["Lisbon", "Porto"]23. Nested maps
use std::collections::HashMap;
fn main() {
let mut permissions: HashMap<&str, HashMap<&str, bool>> = HashMap::new();
permissions.entry("ana").or_default().insert("deploy", true);
permissions.entry("ana").or_default().insert("billing", false);
permissions.entry("ben").or_default().insert("deploy", false);
let can = |user: &str, action: &str| -> bool {
permissions.get(user).and_then(|p| p.get(action)).copied().unwrap_or(false)
};
println!("{} {} {} {}", can("ana", "deploy"), can("ana", "billing"), can("ben", "deploy"), can("zed", "deploy"));
}true false false false24. Capacity
Like Vec, a map grows automatically. with_capacity avoids rehashing when you know the size.
use std::collections::HashMap;
fn main() {
let mut index: HashMap<u32, String> = HashMap::with_capacity(1_000);
let before = index.capacity();
for i in 0..1_000 {
index.insert(i, format!("item-{i}"));
}
println!("capacity held: {}", index.capacity() == before);
println!("{:?}", index.get(&999));
}capacity held: true
Some("item-999")25. Realistic example: memoized Fibonacci
use std::collections::HashMap;
fn fib(n: u64, memo: &mut HashMap<u64, u64>) -> u64 {
if n <= 1 {
return n;
}
if let Some(&cached) = memo.get(&n) {
return cached;
}
let value = fib(n - 1, memo) + fib(n - 2, memo);
memo.insert(n, value);
value
}
fn main() {
let mut memo = HashMap::new();
println!("fib(90) = {}", fib(90, &mut memo));
println!("memo holds {} values", memo.len());
}fib(90) = 2880067194370816120
memo holds 89 valuesWithout the memo, fib(90) would take around 10¹⁸ calls. With it, each value is computed once.
26. Realistic example: an inventory system
use std::collections::HashMap;
#[derive(Debug)]
enum StockError {
Unknown(String),
Insufficient { sku: String, wanted: u32, have: u32 },
}
struct Inventory {
stock: HashMap<String, u32>,
}
impl Inventory {
fn receive(&mut self, sku: &str, qty: u32) {
*self.stock.entry(sku.to_string()).or_insert(0) += qty;
}
fn ship(&mut self, sku: &str, qty: u32) -> Result<u32, StockError> {
let have = self.stock.get_mut(sku).ok_or_else(|| StockError::Unknown(sku.to_string()))?;
if *have < qty {
return Err(StockError::Insufficient { sku: sku.to_string(), wanted: qty, have: *have });
}
*have -= qty;
Ok(*have)
}
fn low_stock(&self, threshold: u32) -> Vec<(&str, u32)> {
let mut low: Vec<(&str, u32)> = self.stock.iter().filter(|&(_, &q)| q <= threshold).map(|(s, &q)| (s.as_str(), q)).collect();
low.sort();
low
}
}
fn main() {
let mut inv = Inventory { stock: HashMap::new() };
inv.receive("KB-1", 10);
inv.receive("MS-2", 3);
inv.receive("KB-1", 5);
println!("{:?}", inv.ship("KB-1", 12));
println!("{:?}", inv.ship("MS-2", 5));
println!("{:?}", inv.ship("XX-9", 1));
println!("low stock: {:?}", inv.low_stock(3));
}Ok(3)
Err(Insufficient { sku: "MS-2", wanted: 5, have: 3 })
Err(Unknown("XX-9"))
low stock: [("KB-1", 3), ("MS-2", 3)]27. Realistic example: grouping anagrams
The key is each word's letters in sorted order, so all anagrams share a key.
use std::collections::HashMap;
fn main() {
let words = ["listen", "silent", "enlist", "google", "inlets", "banana", "gogole", "cat", "act"];
let mut groups: HashMap<String, Vec<&str>> = HashMap::new();
for w in words {
let mut letters: Vec<char> = w.chars().collect();
letters.sort_unstable();
let key: String = letters.into_iter().collect();
groups.entry(key).or_default().push(w);
}
let mut result: Vec<Vec<&str>> = groups.into_values().filter(|g| g.len() > 1).collect();
result.sort();
println!("{:?}", result);
}[["cat", "act"], ["google", "gogole"], ["listen", "silent", "enlist", "inlets"]]28. Realistic example: a per-user rate limiter
Each user has a list of recent request timestamps. Requests older than the window are dropped before checking the limit.
use std::collections::HashMap;
struct RateLimiter {
window_ms: u64,
max_requests: usize,
history: HashMap<String, Vec<u64>>,
}
impl RateLimiter {
fn allow(&mut self, user: &str, now_ms: u64) -> bool {
let times = self.history.entry(user.to_string()).or_default();
times.retain(|&t| now_ms - t < self.window_ms);
if times.len() < self.max_requests {
times.push(now_ms);
true
} else {
false
}
}
}
fn main() {
let mut limiter = RateLimiter { window_ms: 1_000, max_requests: 3, history: HashMap::new() };
let requests = [("ana", 0), ("ana", 100), ("ben", 150), ("ana", 200), ("ana", 300), ("ana", 1_050), ("ana", 1_120)];
for (user, t) in requests {
println!("t={t:>5} {user}: {}", if limiter.allow(user, t) { "allowed" } else { "429 Too Many Requests" });
}
}t= 0 ana: allowed
t= 100 ana: allowed
t= 150 ben: allowed
t= 200 ana: allowed
t= 300 ana: 429 Too Many Requests
t= 1050 ana: allowed
t= 1120 ana: allowed29. Realistic example: an inverted index for search
A search index maps each word to the set of documents containing it. A query intersects the lists.
use std::collections::HashMap;
fn build_index<'a>(docs: &[(u32, &'a str)]) -> HashMap<String, Vec<u32>> {
let mut index: HashMap<String, Vec<u32>> = HashMap::new();
for &(id, text) in docs {
for word in text.split(|c: char| !c.is_alphanumeric()).filter(|w| !w.is_empty()) {
let ids = index.entry(word.to_lowercase()).or_default();
if ids.last() != Some(&id) {
ids.push(id);
}
}
}
index
}
fn search(index: &HashMap<String, Vec<u32>>, query: &str) -> Vec<u32> {
let mut result: Option<Vec<u32>> = None;
for word in query.split_whitespace() {
let ids = index.get(&word.to_lowercase()).cloned().unwrap_or_default();
result = Some(match result {
None => ids,
Some(prev) => prev.into_iter().filter(|id| ids.contains(id)).collect(),
});
}
result.unwrap_or_default()
}
fn main() {
let docs = [
(1, "Rust ownership explained"),
(2, "Borrowing and ownership in Rust"),
(3, "Java garbage collection"),
(4, "Rust iterators, fast and lazy"),
];
let index = build_index(&docs);
for q in ["rust", "rust ownership", "java", "go"] {
println!("{q:<15} -> {:?}", search(&index, q));
}
}rust -> [1, 2, 4]
rust ownership -> [1, 2]
java -> [3]
go -> []30. Realistic example: a two-level cache with statistics
use std::collections::HashMap;
struct PriceCache {
by_sku: HashMap<String, u64>,
hits: u32,
misses: u32,
}
impl PriceCache {
fn price(&mut self, sku: &str, fetch: impl FnOnce(&str) -> u64) -> u64 {
if let Some(&p) = self.by_sku.get(sku) {
self.hits += 1;
return p;
}
self.misses += 1;
let p = fetch(sku);
self.by_sku.insert(sku.to_string(), p);
p
}
}
fn main() {
let mut cache = PriceCache { by_sku: HashMap::new(), hits: 0, misses: 0 };
let catalog = HashMap::from([("KB-1", 12_900), ("MS-2", 3_900)]);
let mut total = 0;
for sku in ["KB-1", "MS-2", "KB-1", "KB-1", "MS-2", "XX-0"] {
total += cache.price(sku, |s| {
println!(" fetching {s} from catalog service");
catalog.get(s).copied().unwrap_or(0)
});
}
println!("total {total}, hits {} misses {}", cache.hits, cache.misses);
} fetching KB-1 from catalog service
fetching MS-2 from catalog service
fetching XX-0 from catalog service
total 46500, hits 3 misses 331. Realistic example: finding pairs that sum to a target
The classic "two sum" problem: a map from each value to its index turns an O(n²) search into O(n).
use std::collections::HashMap;
fn two_sum(prices: &[i64], budget: i64) -> Option<(usize, usize)> {
let mut seen: HashMap<i64, usize> = HashMap::with_capacity(prices.len());
for (i, &p) in prices.iter().enumerate() {
if let Some(&j) = seen.get(&(budget - p)) {
return Some((j, i));
}
seen.insert(p, i);
}
None
}
fn main() {
let gift_prices = [2_500, 1_200, 4_300, 800, 3_700];
println!("{:?}", two_sum(&gift_prices, 4_900));
println!("{:?}", two_sum(&gift_prices, 100));
}Some((1, 4))
NoneSummary
HashMap<K, V>needs keys that areEq + Hash. Derive them for your own key types; floats can't be keys.insertmoves owned keys and values in and returns the old value.get,get_mut,remove, andcontains_keytake a reference to the key, andStringkeys can be looked up with&str.map[key]panics on a missing key;getreturns anOption.- The entry API (
entry(k).or_insert(v),or_insert_with,or_default,and_modify) does "find or create, then update" in one lookup. - Iteration order is unspecified. Sort for display, or use
BTreeMapfor ordered keys. - Common patterns: counting, grouping (
HashMap<K, Vec<V>>), caching, indexing, and inverting.
Next: HashSet.
