Rust Gotcha: last() on DoubleEndedIterator

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 DoubleEndedIterators, 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()
  1. Except, of course, that the compiler might be to optimize all of that away. ↩︎