Lifetimes and iterators
Today I continued reading Rust for Rustaceans. I made it through the Lifetimes chapter.
Back in the day, I remember learning about lifetimes. The notation is a bit weird: 'a.
It can look like this:
fn print_refs<'a, 'b>(x: &'a i32, y: &'b i32) {
println!("x is {} and y is {}", x, y);
} Where 'a and 'b are explicit lifetime annotations.
In reality, every reference has a lifetime. Most of the time, Rust can infer it, so you don’t need to write it explicitly.
Lifetime annotations are useful when we need to describe the relationship between references, especially when the compiler can’t infer that relationship on its own.
For example:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
} Here, 'a expresses that the returned reference is tied to the lifetimes of the input references.
The important thing is that lifetime annotations don’t make references live longer or change the runtime behavior of the program. They describe constraints that must already be true.
These aspects are why understanding the code’s intention and design is so useful.
The lifetime examples in the book are quite advanced. Right now, if I had to solve some of them without looking - like say, in a coding interview - you’d probably see me start to sweat.
I definitely need to practice them more, especially in a real program. That’s still the best way to learn, IMO.
Then I went back to revise Jon Gjengset’s video on iterators.
Iterators took me back to traits.
Traits are one of my favorite features of Rust. A trait is a way to define shared behavior that different types can implement. You can think of it a bit like an interface in TypeScript, but more powerful.
And Iterator is a trait:
https://doc.rust-lang.org/rust-by-example/trait/iter.html
It’s one of Rust’s fundamental traits. It represents something that can produce a sequence of values, one at a time.
At its core, it looks roughly like this:
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
} An iterator must define the type of item it produces and how to get the next item.
For example, suppose we have a Vec:
let v = vec![10, 20, 30];
let mut iter = v.iter();
println!("{:?}", iter.next()); // Some(&10)
println!("{:?}", iter.next()); // Some(&20)
println!("{:?}", iter.next()); // Some(&30)
println!("{:?}", iter.next()); // None Every time we call .next(), the iterator advances and returns an Option.
It returns Some(item) while there are still items left, and None once the iterator is exhausted.
An important detail is that .next() does not consume the iterator itself. It takes &mut self, which means it mutably borrows the iterator and updates its internal state.
That’s why we can keep calling .next() repeatedly on the same iterator.
This weekend I want to finish the video & also advance on my CLI program I’m working on.