How to handle missing keys with the entry API
Short answer: map.entry(key) hands back an Entry, and or_insert() puts a default
value in when the key is missing — either way you get a &mut V back, so one call does what
a get-then-insert pair does with two lookups.
From a fresh project (cargo new entry-demo — the name is mine, and nothing below prints
it), replace the contents of src/main.rs with each block and run it with cargo run.
The shape it replaces
Section titled “The shape it replaces”Adding five points to a player means one of two things depending on whether that player is already in the map, and the longhand form says both out loud:
use std::collections::HashMap;
fn main() { let mut scores: HashMap<&str, i32> = HashMap::new(); scores.insert("Ada", 10);
match scores.get_mut("Ada") { Some(points) => *points += 5, None => { scores.insert("Ada", 5); } } match scores.get_mut("Grace") { Some(points) => *points += 5, None => { scores.insert("Grace", 5); } } println!("{}", scores["Ada"]); println!("{}", scores["Grace"]);}155“Ada” cost two lookups: get_mut found her, so no insert ran. “Grace” cost two as well,
the second one the insert. The entry API merges the two branches into one call.
or_insert
Section titled “or_insert”use std::collections::HashMap;
fn main() { let mut scores: HashMap<&str, i32> = HashMap::new(); *scores.entry("Ada").or_insert(0) += 5; *scores.entry("Ada").or_insert(0) += 5; *scores.entry("Grace").or_insert(0) += 5; println!("{}", scores["Ada"]); println!("{}", scores["Grace"]);}105The standard library describes or_insert as “ensur[ing] a value is in the entry by
inserting the default if empty”. The 0 is built on every call, present or not — free for
an integer, wasteful for anything expensive. or_insert_with takes a closure instead and
only calls it on a miss; or_default inserts V::default():
use std::collections::HashMap;
fn main() { let mut scores: HashMap<&str, i32> = HashMap::new(); scores.insert("Ada", 10);
let mut built = 0; *scores.entry("Ada").or_insert_with(|| { built += 1; 0 }) += 5; *scores.entry("Grace").or_insert_with(|| { built += 1; 0 }) += 5; println!("{}", built); println!("{}", scores["Ada"]); println!("{}", scores["Grace"]);
let mut hits: HashMap<&str, i32> = HashMap::new(); *hits.entry("home").or_default() += 1; *hits.entry("home").or_default() += 1; println!("{}", hits["home"]);}11552The 1 is the proof: “Ada” was already there, so her default was never built. “Grace” was
a miss, so hers was.
When the key is owned
Section titled “When the key is owned”get takes any borrowed form of the key — map.get("Ada") works on a
HashMap<String, i32> — but entry takes the key by value, so the borrowed form is a type
error:
use std::collections::HashMap;
fn main() { let mut scores: HashMap<String, i32> = HashMap::new(); *scores.entry("Ada").or_insert(0) += 5; println!("{}", scores.len());}error[E0308]: mismatched types --> src/main.rs:5:19 | 5 | *scores.entry("Ada").or_insert(0) += 5; | ----- ^^^^^ expected `String`, found `&str` | | | arguments to this method are incorrect |help: the return type of this call is `&'static str` due to the type of the argument passed --> src/main.rs:5:6 | 5 | *scores.entry("Ada").or_insert(0) += 5; | ^^^^^^^^^^^^^-----^ | | | this argument influences the return type of `entry`note: method defined here --> /Users/rango/.rustup/toolchains/1.99.0-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/collections/hash/map.rs:1012:12 |1012 | pub fn entry(&mut self, key: K) -> Entry<'_, K, V, A> { | ^^^^^help: try using a conversion method | 5 | *scores.entry("Ada".to_string()).or_insert(0) += 5; | ++++++++++++
For more information about this error, try `rustc --explain E0308`.The compiler’s own suggestion is the fix: "Ada".to_string(). The cost is an allocation on
every call, hit or miss — a counting allocator measured one per call where get_mut costs
none. In a hot loop over a String-keyed map, that changes the maths.
and_modify
Section titled “and_modify”When the same key can arrive both ways, and_modify runs a closure over the existing
value and chains into or_insert for the miss:
use std::collections::HashMap;
fn main() { let mut scores: HashMap<&str, i32> = HashMap::new(); scores .entry("Ada") .and_modify(|points| *points += 5) .or_insert(5); scores .entry("Ada") .and_modify(|points| *points += 5) .or_insert(5); scores .entry("Grace") .and_modify(|points| *points += 5) .or_insert(5); println!("{}", scores["Ada"]); println!("{}", scores["Grace"]);}105When neither helper fits, entry returns an enum you can match on: Entry::Occupied and
Entry::Vacant, with insert on the vacant side and get, get_mut and key on the
occupied one. BTreeMap has the same entry API, owned-key rule included.
Related
Section titled “Related”- Why can’t I use f64 as a HashMap key? — the other half of a map key’s contract
- Which collection should you use? — when the answer isn’t a HashMap
- How to get an element from a Vec without panicking — lookups that answer with an Option, on a Vec
Next in this chapter: the difference between HashMap and BTreeMap.