25. π Checkpoint: traits and lifetimes
1. Why does this fail, and what are two fixes?
// DOES NOT COMPILE
fn spawn_logger(prefix: &str) -> std::thread::JoinHandle<()> {
std::thread::spawn(|| println!("{prefix}: started"))
}
fn main() { spawn_logger("app").join().unwrap(); }
Answer
thread::spawn requires its closure to be 'static (Β§19.4), because the
thread may outlive the caller's stack frame, and this closure borrows
prefix. The compiler reports E0373 (closure may outlive the current
function) and E0521 (borrowed data escapes the function). Fix 1: own the data with
let prefix = prefix.to_string(); and then move || .... Fix 2: if the
caller waits for the thread before returning, use std::thread::scope
(Ch 28), which allows borrowing.
2. Generic or dyn? You are writing a function that compresses data
with one of three algorithms chosen by a config value at startup, and it
runs once per request. Which do you use?
Answer
Box<dyn Compressor> (or an enum of the three). The algorithm is chosen
at runtime, so you need a single type that can hold any of them. A virtual
call once per request costs nothing measurable. Use generics when the type
is known at compile time and the call is in a tight inner loop.
3. Predict: does fn first<'a>(x: &'a str, y: &str) -> &'a str { x } compile? What about returning y?
Answer
Returning x compiles: the signature says the output borrows from x.
Returning y fails, because y has a different, unrelated lifetime. The
compiler suggests adding 'a to y (E0621, explicit lifetime required).
This is the point of annotations: they are a contract that the function body
is checked against.
4. Write it: implement Iterator for struct Countdown(u32) that
yields 3, 2, 1 for Countdown(3).
One correct answer
struct Countdown(u32);
impl Iterator for Countdown {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.0 == 0 { return None; }
self.0 -= 1;
Some(self.0 + 1)
}
}
fn main() { println!("{:?}", Countdown(3).collect::<Vec<_>>()); } // [3, 2, 1]