Skip to Content
Course content

17: Lifetimes Explained

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Look, I'll be honest with you: lifetimes are usually the point where people start to feel like Rust is fighting them. It's the steepest part of the learning curve. But once you stop thinking of them as "magic symbols" and start seeing them as a way of describing a relationship, it all clicks.

Think of a lifetime like a library book loan. When you check out a book, the library gives you a slip of paper (a reference) that says you have access to that specific book. The rule is simple: the slip of paper is only valid as long as the book actually exists in the library's collection. If the library burns down or the book is permanently discarded, that slip of paper becomes a useless piece of trash. If you tried to use it to go get the book, you'd be pointing at a void. In C, you'd just crash or read garbage memory. In Rust, the "Librarian" (the borrow checker) refuses to let you even leave the building if there's any chance the book might be gone before your slip expires.

Mapping the Library to the Code

In Rust, a lifetime is just a name for the scope in which a piece of data is valid. Most of the time, Rust handles this for you automatically through "elision," but when you start passing references into functions and returning references back out, the compiler needs you to be explicit about the relationship.

  • The Book is the actual data sitting on the heap or stack.
  • The Loan Slip is the reference &T.
  • The Loan Period is the lifetime 'a.

When you see 'a, don't think of it as a variable that changes. Think of it as a label. You're telling Rust: "Hey, this returned reference will live at least as long as the shortest-lived input reference."

The "Longest String" Dilemma

Let's look at a specific case where the compiler will yell at you. Imagine you're writing a function that takes two string slices and returns the longer one. You might try this:

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

This won't compile. Why? Because the compiler doesn't know if the returned reference points to x or y. If x lives for 10 minutes but y only lives for 5 seconds, and the function returns y, the caller might try to use that reference after 6 seconds—at which point y is gone. The Librarian stops you right there.

To fix it, we use generic lifetime annotations. Notice we aren't changing how long the variables live; we're just describing the relationship:

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

By using 'a on all three, you're promising: "The return value will be valid for as long as both x and y are valid." The compiler now knows to truncate the lifetime of the result to the shortest of the two inputs.

Tying Lifetimes to Structs

Functions are one thing, but when you put a reference inside a struct, you're essentially saying this struct is a "loan slip" that depends on some external data. You have to name that dependency.

struct BookReview<'a> {
    book_title: &'a str,
    review_text: &'a str,
}

If you didn't have 'a here, Rust would be terrified. It wouldn't know if BookReview was outliving the strings it points to. By adding the annotation, you're telling Rust: "This BookReview instance cannot possibly outlive the strings it references." If you try to drop the string but keep the BookReview, the compiler will catch it.

The 'static Escape Hatch

You'll often see 'static. This is a special lifetime that lasts for the entire duration of the program. String literals are the most common example. When you write let s = "Hello";, that string is baked directly into the binary. It never gets dropped, so it's &'static str. It's the only loan that never expires.




📋 Practical Task

Build a Log-Message Parser

Your goal is to create a LogEntry struct that doesn't copy data, but instead holds references to a raw log string. This is a common performance optimization in high-throughput systems to avoid unnecessary allocations.

Requirements:

  • Create a struct named LogEntry that contains two fields: level (a reference to a string) and message (a reference to a string).
  • Implement a function parse_log(log: &str) -> LogEntry.
  • The function should split the input string at the first colon (:). The part before the colon is the level, and the part after is the message.
  • Ensure you use the correct lifetime annotations so that the LogEntry cannot outlive the original log string.

Test Case:

fn main() {
    let raw_log = String::from("ERROR: Disk full on /dev/sda1");
    let entry = parse_log(&raw_log);
    println!("Level: {}, Message: {}", entry.level, entry.message);
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.