tl;dr: don’t call last()
on a DoubleEndedIterator
Edit: And, now it is a Clippy lint!
How do you efficiently get the last part of a space-separated string in Rust?
It will be obvious to some, but the obvious answer of s.split(' ').last()
is wrong. The mistake is easy to make; I encountered it in a recent MR I reviewed, and I realized I used it in some of my own code.
Let’s see why.
If it were Python, s.split(' ')
would return a list
with all the parts of s
. In other words, this code wastes precious CPU cycles extracting parts you do not care about. In Python, you would avoid most of that by using s.rsplit(' ', 1)[1]
instead.
But we are talking about ✨ Rust ✨ here, with which everything is ⚡️ blazingly fast ⚡️. So, of course, str::split()
returns an instance of Split
, which an Iterator
that lazily splits the string. That is, it only looks for the next part of the string when you call next()
.
But what about last()
?
Split
implements DoubleEndedIterator
. This means you can iterate from the end by calling next_back()
. You would think that, for DoubleEndedIterator
s, last()
is implemented by simply calling next_back()
. Alas, Rust traits do not allow for specialization, so, when you call last()
on a DoubleEndedIterator
, you are really calling Iterator::last()
, which just exhausts the iterator until the last Some
value.
You can see that in action by running that code:
struct Test;
impl Iterator for Test {
type Item = &'static str;
fn next(&mut self) -> Option<Self::Item> {
println!("next()");
Some("next()")
}
}
impl DoubleEndedIterator for Test {
fn next_back(&mut self) -> Option<Self::Item> {
println!("next_back()");
Some("next_back()")
}
}
fn main() {
let mut t = Test;
println!("{:?}", t.next());
println!("{:?}", t.next_back());
// loops forever since next() never returns None
println!("{:?}", t.last());
}
This will print:
next()
Some("next()")
next_back()
Some("next_back()")
next()
next()
next()
next()
…
(repeats forever since next() never returns None)
In short, s.split(' ').last()
in Rust is no better than s.split(' ')[-1]
in Python1. If you want to properly get the last value of a DoubleEndedIterator
, remember to use next_back()
:
s.split(' ').next_back()
Or, equivalently, rev().next()
:
s.split(' ').rev().next()
- Except, of course, that the compiler might be to optimize all of that away. ↩︎