#261 Jul 15, 2026

261. String::retain — Delete Characters In Place, No New Allocation

Stripping characters with replace("-", "") builds a brand-new String just to throw characters away. retain deletes them in the buffer you already own.

The trap

This morning’s bite (260) covered replacen — but both replace and replacen always return a fresh String, even when the “replacement” is deleting. Same story with the iterator route:

1
2
3
4
5
6
7
let phone = String::from("+49 (0)30 901820");

// both of these allocate a whole new String
let a = phone.replace(|c: char| !c.is_ascii_digit(), "");
let b: String = phone.chars().filter(char::is_ascii_digit).collect();
assert_eq!(a, "49030901820");
assert_eq!(b, a);

If you’re cleaning strings in a loop, that’s one allocation per string, per pass — for data you already had in a perfectly good buffer.

The fix

String::retain keeps every char the closure approves and shifts the rest out, in place, in one O(n) pass:

1
2
3
4
5
6
7
let mut phone = String::from("+49 (0)30 901820");
let cap = phone.capacity();

phone.retain(|c| c.is_ascii_digit());

assert_eq!(phone, "49030901820");
assert_eq!(phone.capacity(), cap); // same buffer

No new allocation, and the capacity stays put — ready for push_str later. It reads as intent, too: “keep digits” instead of “replace non-digits with nothing”.

One caveat

The closure sees chars in order, exactly once, and retain keeps what returns true — it’s a keep-list, not a kill-list. To delete matches, negate:

1
2
3
let mut s = String::from("no_more_underscores");
s.retain(|c| c != '_');
assert_eq!(s, "nomoreunderscores");

Vec<T> and VecDeque<T> have the same method, so the pattern transfers. When you need to substitute text, replace/replacen earn their allocation — but when you’re only deleting, retain does it where the string already lives.

← Previous 260. str::replacen — Replace the First N Matches, Not Every Single One Next → 262. String::insert_str — Prepend or Splice Text Without Rebuilding the String