Intermediate Level

Lifetimes

Chapter 13: Lifetimes ā³

Understand how Rust manages memory through lifetimes to prevent dangling references.

Understanding Lifetimes

The Borrow Checker

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {}", r); //          |
}                         // ---------+

This code won't compile because r references x which goes out of scope before r is used.

Lifetime Annotations

Basic Syntax

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let string1 = String::from("long string is long");
    let string2 = String::from("xyz");
    
    let result = longest(&string1, &string2);
    println!("The longest string is: {}", result);
}

Multiple Lifetime Parameters

fn first_word<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
    let words: Vec<&str> = x.split_whitespace().collect();
    words[0]
}

fn main() {
    let string1 = String::from("Hello world");
    let string2 = String::from("Rust programming");
    
    let result = first_word(&string1, &string2);
    println!("First word: {}", result);
}

Lifetime Annotations in Structs

Structs with References

struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("Could not find a '.'");
    let i = ImportantExcerpt {
        part: first_sentence,
    };
    
    println!("Excerpt: {}", i.part);
}

Lifetime Elision

Rules for Automatic Lifetime Inference

// Rule 1: Each parameter that is a reference gets its own lifetime parameter
fn first_word(s: &str) -> &str { // Actually: fn first_word<'a>(s: &'a str) -> &'a str
    let words: Vec<&str> = s.split_whitespace().collect();
    words[0]
}

// Rule 2: If there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters
fn return_input(x: &str) -> &str { // Actually: fn return_input<'a>(x: &'a str) -> &'a str
    x
}

// Rule 3: If there are multiple input lifetime parameters, but one of them is &self or &mut self,
// the lifetime of self is assigned to all output lifetime parameters
impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 { // Actually: fn level<'a>(&'a self) -> i32
        3
    }
    
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        // Actually: fn announce_and_return_part<'a, 'b>(&'a self, announcement: &'b str) -> &'a str
        println!("Attention please: {}", announcement);
        self.part
    }
}

Static Lifetimes

References that Live for the Entire Program

let s: &'static str = "I have a static lifetime.";

// Static lifetimes are often used for string literals
// because they're stored in the program's binary

Generic Lifetimes with Traits

Combining Lifetimes and Traits

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    println!("Announcement! {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let string1 = String::from("long string is long");
    let string2 = String::from("xyz");
    
    let result = longest_with_an_announcement(
        &string1,
        &string2,
        "Today is someone's birthday!"
    );
    
    println!("The longest string is: {}", result);
}

Lifetime Annotations in Methods

Implementing Methods with Lifetimes

struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
    fn new(part: &'a str) -> ImportantExcerpt<'a> {
        ImportantExcerpt { part }
    }
    
    fn level(&self) -> i32 {
        3
    }
    
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
    
    fn part(&self) -> &str {
        self.part
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("Could not find a '.'");
    let excerpt = ImportantExcerpt::new(first_sentence);
    
    println!("Level: {}", excerpt.level());
    println!("Part: {}", excerpt.part());
    println!("Announcement: {}", excerpt.announce_and_return_part("Chapter 1"));
}

Practical Examples

Example 1: Config Parser with Lifetimes

struct Config<'a> {
    name: &'a str,
    value: &'a str,
}

impl<'a> Config<'a> {
    fn new(name: &'a str, value: &'a str) -> Config<'a> {
        Config { name, value }
    }
    
    fn get_name(&self) -> &str {
        self.name
    }
    
    fn get_value(&self) -> &str {
        self.value
    }
    
    fn display(&self) {
        println!("{}: {}", self.name, self.value);
    }
}

fn parse_config_line<'a>(line: &'a str) -> Option<Config<'a>> {
    let parts: Vec<&str> = line.split('=').collect();
    if parts.len() == 2 {
        Some(Config::new(parts[0].trim(), parts[1].trim()))
    } else {
        None
    }
}

fn main() {
    let config_line = "database_url = postgresql://localhost:5432/mydb";
    
    if let Some(config) = parse_config_line(config_line) {
        config.display();
        println!("Name: {}", config.get_name());
        println!("Value: {}", config.get_value());
    }
}

Example 2: Text Processing with Lifetimes

struct TextProcessor<'a> {
    text: &'a str,
    processed_lines: Vec<&'a str>,
}

impl<'a> TextProcessor<'a> {
    fn new(text: &'a str) -> TextProcessor<'a> {
        TextProcessor {
            text,
            processed_lines: Vec::new(),
        }
    }
    
    fn split_lines(&mut self) -> &Vec<&'a str> {
        self.processed_lines = self.text.lines().collect();
        &self.processed_lines
    }
    
    fn find_line_containing(&self, pattern: &str) -> Option<&'a str> {
        self.processed_lines.iter()
            .find(|line| line.contains(pattern))
            .copied()
    }
    
    fn get_longest_line(&self) -> &'a str {
        self.processed_lines.iter()
            .max_by_key(|line| line.len())
            .unwrap_or(&"")
    }
}

fn main() {
    let text = "Rust is a systems programming language
It's known for memory safety and performance
Rust prevents segfaults and guarantees thread safety";
    
    let mut processor = TextProcessor::new(text);
    processor.split_lines();
    
    if let Some(line) = processor.find_line_containing("memory") {
        println!("Found line: {}", line);
    }
    
    println!("Longest line: {}", processor.get_longest_line());
}

Common Mistakes

āŒ Forgetting Lifetime Annotations

// This won't compile without lifetime annotations
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

āœ… Adding Proper Lifetime Annotations

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

āŒ Returning References to Local Variables

fn bad_function<'a>() -> &'a str {
    let s = String::from("Hello");
    &s // Error: s doesn't live long enough
}

āœ… Returning Static References or Owned Values

fn good_function_static() -> &'static str {
    "Hello" // String literals have static lifetime
}

fn good_function_owned() -> String {
    String::from("Hello") // Return owned value instead
}

Key Takeaways

  • āœ… Lifetimes ensure references are valid for as long as needed
  • āœ… The borrow checker prevents dangling references at compile time
  • āœ… Lifetime annotations specify how long references live
  • āœ… Most lifetime annotations can be inferred through elision rules
  • āœ… Static lifetimes ('static) live for the entire program duration
  • āœ… Structs with references must have lifetime annotations
  • āœ… Methods can have lifetime annotations in their signatures
  • āœ… Follow Rust naming conventions (lifetime names start with ' and are typically lowercase)

Ready for Chapter 14? → Testing

šŸ¦€ Rust Programming Tutorial

Learn from Zero to Advanced

Built with Next.js and Tailwind CSS • Open Source