Skip to main content

Rust Lifetimes

Lifetimes are one of Rust's most powerful yet initially challenging concepts to grasp. They're a key part of how Rust ensures memory safety without a garbage collector. By the end of this tutorial, you'll understand what lifetimes are, why they matter, and how to use them in your code.

What Are Lifetimes?

In Rust, a lifetime is a construct the compiler uses to ensure all borrows are valid. Simply put, a lifetime is the scope for which a reference is valid.

Every reference in Rust has a lifetime, which is the scope where that reference is valid. Most of the time, lifetimes are implicit and inferred by the compiler. However, sometimes we need to annotate relationships using explicit lifetime parameters.

Why Do We Need Lifetimes?

Rust's main goal is memory safety without a garbage collector. Lifetimes help achieve this by:

  1. Preventing dangling references - references to data that has been freed
  2. Ensuring data doesn't go out of scope before its references do
  3. Tracking relationships between different references

Let's look at an example of a dangling reference that Rust prevents:

rust
fn main() {
let r;
{
let x = 5;
r = &x; // We borrow `x` here
} // `x` goes out of scope here, so `r` would be a dangling reference

println!("r: {}", r); // Error! Rust prevents this code from compiling
}

Output:

error[E0597]: `x` does not live long enough
--> src/main.rs:5:13
|
5 | r = &x;
| ^^ borrowed value does not live long enough
6 | }
| - `x` dropped here while still borrowed
7 |
8 | println!("r: {}", r);
| - borrow later used here

The Rust compiler tracks the lifetime of each variable and ensures references don't outlive the data they point to.

Lifetime Annotations Syntax

Lifetime annotations don't change how long references live. They simply describe the relationships between the lifetimes of multiple references.

Lifetime annotations have a slightly unusual syntax:

  • They start with an apostrophe (')
  • Are typically lowercase and short (like 'a, 'b, etc.)
  • Are placed after the & in references

Basic Syntax Examples

rust
&i32        // A reference
&'a i32 // A reference with an explicit lifetime
&'a mut i32 // A mutable reference with an explicit lifetime

Where Do We Need Lifetime Annotations?

In most cases, Rust can infer lifetimes through a process called "lifetime elision." However, there are three main scenarios where you'll need to specify lifetimes:

  1. Function signatures with references in parameters or return values
  2. Struct definitions that hold references
  3. Impl blocks for types with lifetimes

Let's explore each case.

Function Lifetimes

When a function takes references as parameters or returns a reference, we sometimes need to tell Rust how those lifetimes relate to each other.

Example: Finding the Longer String

rust
// Without lifetimes (will not compile)
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}

// With lifetimes
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.as_str(), string2.as_str());
println!("The longest string is: {}", result);
}

Output:

The longest string is: long string is long

The <'a> syntax defines a generic lifetime parameter. By using 'a in all three places, we're telling Rust:

  1. Both parameters live at least as long as 'a
  2. The return value lives at least as long as 'a
  3. The actual lifetime used will be the smaller of the lifetimes of x and y

Lifetime Elision Rules

Rust has three rules that allow it to infer lifetimes in many cases:

  1. Each parameter that is a reference gets its own lifetime parameter
  2. If there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters
  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

Because of these rules, you often don't need to specify lifetimes explicitly.

Struct Lifetimes

When a struct holds references, you must annotate the lifetimes:

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

fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let excerpt = Excerpt {
part: first_sentence,
};

println!("Excerpt: {}", excerpt.part);
}

Output:

Excerpt: Call me Ishmael

The lifetime parameter 'a tells Rust that an Excerpt instance cannot outlive the reference it holds in its part field.

The Static Lifetime

The 'static lifetime is special in Rust. It indicates that the reference can live for the entire duration of the program. All string literals have the 'static lifetime:

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

Be cautious with 'static - it's rarely what you want, and using it incorrectly can lead to memory leaks.

Lifetime Bounds

Just like you can have trait bounds on generic types, you can have lifetime bounds:

rust
fn longest_with_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
}
}

Visualizing Lifetimes

Let's visualize how lifetimes work in Rust code:

Real-World Example: A Configuration Parser

Let's implement a simple configuration parser that demonstrates lifetimes in a practical context:

rust
struct Config<'a> {
// These fields hold references to parts of the input string
server_name: &'a str,
db_connection: &'a str,
max_connections: u32,
}

fn parse_config<'a>(config_str: &'a str) -> Result<Config<'a>, &'static str> {
let mut server_name = "";
let mut db_connection = "";
let mut max_connections = 0;

for line in config_str.lines() {
let parts: Vec<&str> = line.splitn(2, '=').collect();
if parts.len() != 2 {
continue;
}

let key = parts[0].trim();
let value = parts[1].trim();

match key {
"SERVER_NAME" => server_name = value,
"DB_CONNECTION" => db_connection = value,
"MAX_CONNECTIONS" => {
max_connections = value.parse().unwrap_or(10);
}
_ => {}
}
}

if server_name.is_empty() {
return Err("SERVER_NAME is required");
}

Ok(Config {
server_name,
db_connection,
max_connections,
})
}

fn main() {
let config_text = "
SERVER_NAME = production-server
DB_CONNECTION = postgres://user:pass@localhost/db
MAX_CONNECTIONS = 50
";

match parse_config(config_text) {
Ok(config) => {
println!("Server: {}", config.server_name);
println!("Database: {}", config.db_connection);
println!("Max Connections: {}", config.max_connections);
}
Err(e) => println!("Configuration error: {}", e),
}
}

Output:

Server: production-server
Database: postgres://user:pass@localhost/db
Max Connections: 50

In this example:

  1. Config<'a> holds references to slices of the original config string
  2. The parse_config function ensures these references don't outlive the input string
  3. The lifetime parameter 'a creates a relationship between the input and output

Common Lifetime Patterns

Here are some common patterns you'll encounter:

  1. Multiple input references, one output: The output refers to one of the inputs

    rust
    fn first_word<'a>(s: &'a str) -> &'a str { /* ... */ }
  2. Independent references: Different lifetime parameters for independent references

    rust
    fn different_lifetimes<'a, 'b>(x: &'a str, y: &'b str) -> &'a str { /* ... */ }
  3. Struct with references: Struct holding references with explicit lifetimes

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

Advanced Lifetime Concepts

As you get more comfortable with Rust, you might encounter these advanced concepts:

Lifetime Subtyping

Just as one type can be a subtype of another, one lifetime can be a subtype of another:

rust
fn subtype_example<'a, 'b: 'a>(x: &'a str, y: &'b str) -> &'a str {
// 'b outlives 'a
x
}

The 'b: 'a syntax means "'b outlives 'a" - any reference with lifetime 'b will live at least as long as references with lifetime 'a.

Higher-Ranked Trait Bounds (HRTB)

Sometimes you'll see syntax like for<'a> in trait bounds. This means "for all lifetimes 'a":

rust
fn transform<F>(f: F)
where
F: for<'a> Fn(&'a str) -> &'a str,
{
// ...
}

This is used when working with closures or function pointers that themselves take and return references.

Common Lifetime Errors and How to Fix Them

1. "Missing lifetime specifier"

rust
// Error
fn return_reference(s: &str) -> &str {
s
}

// Fix
fn return_reference<'a>(s: &'a str) -> &'a str {
s
}

2. "Borrowed value does not live long enough"

rust
// Error
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("r: {}", r);
}

// Fix
fn main() {
let x = 5;
let r = &x;
println!("r: {}", r);
}

3. "Cannot return reference to local variable"

rust
// Error
fn create_and_return_reference() -> &str {
let s = String::from("hello");
&s // Error: returns a reference to data owned by the current function
}

// Fix
fn create_and_return_string() -> String {
String::from("hello")
}

Summary

Rust's lifetime system is a powerful mechanism that enables memory safety without garbage collection. Let's recap what we've learned:

  1. Lifetimes ensure references are valid for as long as they're used
  2. Most of the time, lifetimes are implicit thanks to Rust's lifetime elision rules
  3. Explicit lifetime annotations describe relationships between references
  4. Structs that hold references need lifetime parameters
  5. The static lifetime 'static is for data that lives for the entire program

While lifetimes may seem complex at first, they become more intuitive with practice. They're an essential part of what makes Rust both powerful and safe.

Exercises

  1. Create a function that takes a string slice and returns the first word (hint: use split_whitespace() or split(' '))
  2. Implement a struct that holds a reference to a line from a text file, along with its line number
  3. Write a function that compares two string slices and returns the one with the most vowels
  4. Challenge: Create a simple text highlighter that takes a text and a pattern and returns the text with the pattern highlighted (as a reference)

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)