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.
| |
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:
| |
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:
| |
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.