Labs ICT
โญ Pro Login

Lifetimes

How Rust tracks how long references live.

3 min read | Rust Tutorial
โญ

Want the full learning experience?

Get structured courses, certificates, projects, and instructor support with LabsICT Pro.

Explore Pro Courses

Lifetimes in Rust

Lifetimes are Rust's way of ensuring references are always valid. They're like compile-time labels that tell the compiler how long references live.

You rarely need to write lifetime annotations explicitly. The compiler figures them out most of the time through lifetime elision rules.

fn main() {
    let string1 = String::from("long string");
    let result;
    {
        let string2 = String::from("hi");
        result = longest(string1.as_str(), string2.as_str());
        println!("Longest: {}", result);
    }
    // result would be invalid here because string2 is dropped
}

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

Lifetime Elision Rules

The compiler applies three rules to figure out lifetimes automatically. First, each parameter gets its own lifetime. Second, if there's exactly one input lifetime, it's assigned to all outputs. Third, if there's a &self or &mut self, its lifetime is assigned to outputs.

// These two signatures are equivalent:

// With explicit lifetimes:
fn first_word_explicit<'a>(s: &'a str) -> &'a str {
    s.split_whitespace().next().unwrap_or("")
}

// With elision (compiler figures out lifetimes):
fn first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap_or("")
}

fn main() {
    let sentence = String::from("hello world");
    let word = first_word(&sentence);
    println!("First word: {}", word);
}

Function Signatures with Lifetimes

When functions return references, you often need to specify which input the output is connected to. This prevents dangling references.

fn longest_word<'a>(sentence: &'a str) -> &'a str {
    let mut longest = "";
    for word in sentence.split_whitespace() {
        if word.len() > longest.len() {
            longest = word;
        }
    }
    longest
}

fn main() {
    let text = String::from("the quick brown fox jumps");
    let word = longest_word(&text);
    println!("Longest word: {}", word);
}

Struct Lifetimes

When structs hold references, you need lifetime annotations to ensure the data outlives the struct.

struct Excerpt<'a> {
    text: &'a str,
}

impl<'a> Excerpt<'a> {
    fn level(&self) -> i32 {
        3
    }

    fn announce(&self, announcement: &str) -> &str {
        println!("Attention: {}", announcement);
        self.text
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let excerpt = Excerpt {
        text: &novel[..15],
    };
    println!("Excerpt: {}", excerpt.text);
    println!("Level: {}", excerpt.level());
}

The 'static Lifetime

The 'static lifetime means the reference lives for the entire program. String literals have 'static lifetime because they're embedded in the binary.

fn main() {
    let s: &'static str = "I live forever";
    println!("{}", s);

    // String literals are always 'static
    let literal = "This is also 'static";
    println!("{}", literal);
}

Why Lifetimes Matter

Lifetimes prevent dangling references, which cause undefined behavior in other languages. Rust catches these bugs at compile time, making your code safer.

You only need explicit annotations when the compiler can't figure out lifetimes automatically. In most cases, especially with function signatures, the compiler handles it for you.

๐Ÿงช Quick Quiz

What syntax annotates lifetimes?