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.

← Previous 200. #[derive(Copy)] and #[inline] — Make Small Types Free to Pass Around