HashMap::retain discards what it removes. When you actually want those values — to log them, ship them, or move them elsewhere — you used to clone the keys and double-walk the map. extract_if, stable since Rust 1.88, hands them back as an iterator.
retain throws the babies out with the bathwater
retain is the natural “filter a map in place” call, but its return type is (). The entries it kicks out vanish:
1
2
3
4
5
6
7
8
9
10
11
12
13
| use std::collections::HashMap;
let mut sessions: HashMap<&str, u32> = HashMap::from([
("alice", 12),
("bob", 0),
("carol", 3),
("dan", 0),
]);
sessions.retain(|_user, hits| *hits > 0);
assert_eq!(sessions.len(), 2);
// Who got dropped? Gone. We can't tell anyone.
|
If you wanted to log the expired sessions, notify their owners, or forward them to another map, retain isn’t enough. The textbook workaround is collect-then-remove:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| use std::collections::HashMap;
let mut sessions: HashMap<String, u32> = HashMap::from([
("alice".into(), 12),
("bob".into(), 0),
("carol".into(), 3),
("dan".into(), 0),
]);
let to_evict: Vec<String> = sessions
.iter()
.filter(|(_, hits)| **hits == 0)
.map(|(k, _)| k.clone()) // forced clone to break the self-borrow
.collect();
let mut evicted = Vec::new();
for k in to_evict {
if let Some(v) = sessions.remove(&k) {
evicted.push((k, v));
}
}
assert_eq!(evicted.len(), 2);
|
Two passes, a throwaway Vec<K>, and a hard K: Clone requirement just so the borrow checker stops yelling.
extract_if is one pass and gives the values back
HashMap::extract_if(pred) returns an iterator that yields (K, V) for every entry whose predicate fires true, removing them from the map as it goes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| use std::collections::HashMap;
let mut sessions: HashMap<&str, u32> = HashMap::from([
("alice", 12),
("bob", 0),
("carol", 3),
("dan", 0),
]);
let evicted: HashMap<&str, u32> =
sessions.extract_if(|_user, hits| *hits == 0).collect();
assert_eq!(evicted.len(), 2);
assert_eq!(sessions.len(), 2);
assert!(sessions.contains_key("alice"));
assert!(sessions.contains_key("carol"));
|
No clone, no temporary Vec, no second walk. The removed entries land in whatever collection you choose — another HashMap, a Vec<(K, V)>, a channel, a log line. HashMap order isn’t guaranteed, but neither was it before.
The predicate gets &mut V, so it can edit survivors
The closure signature is FnMut(&K, &mut V) -> bool. Return true to extract, false to keep — and because you’ve got &mut V in hand either way, you can update entries you choose to leave behind:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| use std::collections::HashMap;
let mut scores: HashMap<&str, i32> = HashMap::from([
("alice", 85),
("bob", 42),
("carol", 91),
("dan", 30),
]);
let failing: HashMap<&str, i32> = scores
.extract_if(|_name, score| {
if *score < 50 {
true // pull this one out
} else {
*score += 5; // bump the survivors
false // keep them
}
})
.collect();
assert_eq!(failing.len(), 2);
assert_eq!(scores[&"alice"], 90);
assert_eq!(scores[&"carol"], 96);
|
One traversal, three jobs: filter, remove-and-collect, and patch in place.
Drop the iterator early and the rest stay
The iterator only removes entries it has visited. If you stop iterating — break, take(n), drop the iterator — anything not yet inspected stays in the map untouched:
1
2
3
4
5
6
7
8
9
10
11
12
13
| use std::collections::HashMap;
let mut nums: HashMap<i32, i32> = (0..10).map(|i| (i, i)).collect();
// Pull at most two even entries out, then stop.
let pulled: Vec<(i32, i32)> = nums
.extract_if(|_, v| *v % 2 == 0)
.take(2)
.collect();
assert_eq!(pulled.len(), 2);
// 10 - 2 = 8 left, regardless of which evens were taken.
assert_eq!(nums.len(), 8);
|
When to reach for it
Whenever you’d otherwise write the clone-keys-and-double-pass shuffle: expiring sessions, draining tasks that hit a deadline, partitioning a cache by tenant, moving “done” items into an archive map. HashSet::extract_if is the same idea for sets — predicate is FnMut(&T) -> bool. The mental rule: reach for retain when the discarded entries are truly garbage, and extract_if when they still have a job to do.