Entry

201. or_insert vs or_insert_with — Don't Build a Default You'll Throw Away

map.entry(k).or_insert(expensive()) builds expensive() on every call — even when the key is already there and the value gets dropped on the floor. Reach for or_insert_with and the default is computed only when it’s actually needed.

The entry API already saves you the contains_key-then-insert double lookup. But there’s a second, quieter cost hiding in or_insert: its argument is an ordinary value, so it’s evaluated before the call, regardless of whether the slot is occupied or vacant.

1
2
3
4
5
6
7
8
9
use std::collections::HashMap;

let mut cache: HashMap<&str, String> = HashMap::new();
cache.insert("hit", "already here".to_string());

// "expensive default".to_string() allocates a fresh String here...
// ...then gets immediately discarded because "hit" is occupied.
cache.entry("hit").or_insert("expensive default".to_string());
assert_eq!(cache["hit"], "already here");

That throwaway allocation happens on every hit. In a hot loop over a mostly-populated map, you’re paying to construct defaults you never store.

or_insert_with takes a closure instead of a value, so the work is deferred until the entry is genuinely vacant:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
use std::collections::HashMap;

let mut map: HashMap<&str, String> = HashMap::new();
map.insert("a", "existing".to_string());

let mut builds = 0;

// "a" is present, so the closure never runs.
map.entry("a").or_insert_with(|| { builds += 1; "default".to_string() });
assert_eq!(builds, 0);

// "b" is vacant, so the closure runs exactly once.
map.entry("b").or_insert_with(|| { builds += 1; "default".to_string() });
assert_eq!(builds, 1);

assert_eq!(map["a"], "existing");
assert_eq!(map["b"], "default");

The rule of thumb: if the default is a plain literal or a cheap Copy value (0, false, None), or_insert is fine and reads cleaner. The moment the default allocates or computes — a Vec::new(), a String, a hash of the key, a database handle — switch to or_insert_with.

When the default depends on the key itself, or_insert_with_key hands the key to the closure so you don’t have to capture it:

1
2
3
4
5
6
7
8
9
use std::collections::HashMap;

let mut sizes: HashMap<String, usize> = HashMap::new();

let n = sizes
    .entry("hello".to_string())
    .or_insert_with_key(|k| k.len());

assert_eq!(*n, 5);

All three still cost a single hash and a single lookup — the entry lands on the slot once and hands you a &mut V. The only thing you’re choosing is when the default gets built: always, or only when it’s needed.

166. Entry::and_modify — Update If Present, Insert If Not, in One Chain

*map.entry(k).or_insert(0) += 1 is the classic Rust counter. The moment the “first time” branch needs to look different from the “next time” branch, that pattern stops fitting — and and_modify slots in.

HashMap::entry(k) returns an Entry enum pointing at the slot for k, occupied or vacant. The famous methods are or_insert(default) and or_insert_with(|| ...). Quieter but often nicer is and_modify: it runs a closure on the value when the key is already there, and does nothing when it isn’t. Chain it with or_insert and you get update if present, insert if not in one expression — with a single lookup.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use std::collections::HashMap;

let mut counts: HashMap<&str, u32> = HashMap::new();

for word in ["rust", "iron", "rust", "rust", "iron"] {
    counts
        .entry(word)
        .and_modify(|n| *n += 1)
        .or_insert(1);
}

assert_eq!(counts["rust"], 3);
assert_eq!(counts["iron"], 2);

For pure counters that’s a tie with *entry.or_insert(0) += 1. The shape really pays off when the two branches store different data. Imagine grouping events by user, where the first event records the user’s name and later events only bump a counter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
use std::collections::HashMap;

#[derive(Debug, PartialEq)]
struct UserStats { name: String, visits: u32 }

let events = [(1u32, "alice"), (2, "bob"), (1, "alice")];
let mut stats: HashMap<u32, UserStats> = HashMap::new();

for (id, name) in events {
    stats
        .entry(id)
        .and_modify(|s| s.visits += 1)
        .or_insert(UserStats { name: name.into(), visits: 1 });
}

assert_eq!(stats[&1], UserStats { name: "alice".into(), visits: 2 });
assert_eq!(stats[&2], UserStats { name: "bob".into(),   visits: 1 });

and_modify takes &mut V so you mutate in place; or_insert produces the initial V. and_modify also returns the Entry back, which is why the chain works — and which means you can stack several modifications before the final or_insert.

31. HashMap's entry API

Want to insert a value into a HashMap only if the key doesn’t exist yet? Skip the double lookup — use the entry API.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use std::collections::HashMap;

let mut scores: HashMap<&str, Vec<u32>> = HashMap::new();

// Instead of checking .contains_key() then inserting:
scores.entry("alice")
    .or_insert_with(Vec::new)
    .push(100);

scores.entry("alice")
    .or_insert_with(Vec::new)
    .push(200);

assert_eq!(scores["alice"], vec![100, 200]);

The entry API returns an Entry enum — either Occupied or Vacant. The convenience methods make common patterns a one-liner:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use std::collections::HashMap;

let mut word_count: HashMap<&str, usize> = HashMap::new();
let words = ["hello", "world", "hello", "rust", "hello"];

for word in words {
    *word_count.entry(word).or_insert(0) += 1;
}

// hello => 3, world => 1, rust => 1

or_insert(val) inserts a default, or_insert_with(|| val) lazily computes it, and or_default() uses the type’s Default. All three return a mutable reference to the value, so you can update in place.