DEV Community

Subesh Yadav
Subesh Yadav

Posted on

🌱 Day 19 of #100DaysOfRust: Understanding Lifetimes in Rust

Today’s Focus: Lifetimes – a unique and powerful concept in Rust that ensures memory safety without needing a garbage collector.


✅ What are Lifetimes?

Lifetimes are a kind of generic used by Rust to ensure references are valid for as long as they need to be. They prevent dangling references and make memory safety possible at compile time.

Think of lifetimes as a promise that references will not outlive the data they point to.


🔍 Why Lifetimes?

Every reference in Rust has a lifetime, but most of the time, the compiler can infer them. However, when it can’t, we need to annotate them explicitly to let the compiler understand how long references should be valid.

📌 Example of a Dangling Reference (Does not compile):

fn main() {
    let r;
    {
        let x = 5;
        r = &x;
    }
    println!("r: {}", r); // ❌ x is dropped, r points to invalid memory
}
Enter fullscreen mode Exit fullscreen mode

✅ Fixed Version:

fn main() {
    let x = 5;
    let r = &x;
    println!("r: {}", r);
}
Enter fullscreen mode Exit fullscreen mode

🧠 The Borrow Checker

Rust’s borrow checker compares lifetimes to ensure references don’t outlive the data they point to. It’s what catches the issue in the first example.


🧪 Lifetimes in Functions

We might write a function like this:

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}
Enter fullscreen mode Exit fullscreen mode

But this won't compile. Rust needs to know how the lifetimes of x and y relate to the returned reference.


✅ Correct Version with Lifetime Annotations:

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

This means the returned reference will live as long as the shortest of the two inputs.


🔧 Lifetime Syntax

  • &i32 → reference
  • &'a i32 → reference with explicit lifetime 'a
  • &'a mut i32 → mutable reference with explicit lifetime 'a

🧪 Real Example:

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";
    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}
Enter fullscreen mode Exit fullscreen mode

⛔ A Non-Compiling Example

fn main() {
    let string1 = String::from("abcd");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {}", result); // ❌ string2 is dropped
}
Enter fullscreen mode Exit fullscreen mode

🧪 Experiment Yourself

Try variations: change lifetimes, scope of variables, and return values. Test how Rust behaves. You'll learn by doing.


📦 Lifetimes in Structs

struct ImportantExcerpt<'a> {
    part: &'a str,
}
Enter fullscreen mode Exit fullscreen mode

This ensures the struct doesn’t outlive the reference it holds.


🧵 Lifetime Elision Rules

Rust tries to infer lifetimes using three rules:

  • Each parameter gets its own lifetime.
  • If there is one input lifetime, it’s assigned to all output lifetimes.
  • If multiple input lifetimes exist and one is &self, its lifetime is assigned to the output.

These rules help reduce the need for manual annotations in many cases.

🧪 Example Using Rule 3 (Methods):

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}
Enter fullscreen mode Exit fullscreen mode

🧊 'static Lifetime

let s: &'static str = "I have a static lifetime.";
Enter fullscreen mode Exit fullscreen mode

Means the data lives for the entire duration of the program. Common with string literals.


🧬 Combining Generics, Trait Bounds, and Lifetimes

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 }
}
Enter fullscreen mode Exit fullscreen mode

🧠 Summary

  • Lifetimes prevent dangling references.
  • They're enforced at compile time via the borrow checker.
  • Most of the time, lifetimes are inferred, but sometimes we need to annotate them.
  • You can even use them in structs, functions, methods, and with traits and generics.
  • This enables Rust to stay fast and safe without a garbage collector.

🔗 Next Up: Writing tests in Rust!

📚 Stay curious, stay consistent!

Top comments (0)