Rust track

Lesson 09 · Part 2: Ownership, Borrowing & Lifetimes

Lifetimes

Lifetime annotations and static lifetimes

Part 2: Ownership, Borrowing & Lifetimes5 min readFree lesson

A lifetime is a label that tells the compiler "the period during which a reference is valid." All references we've seen in Chapters 6–8 actually had lifetimes attached, but the compiler mostly filled them in automatically. In this chapter, we summarize when and why lifetimes need to be specified, and the rules by which the compiler allows them to be omitted.

Learning Objectives

  • Understand why lifetimes are needed from the perspective of preventing dangling references.
  • Learn the basic pattern of specifying 'a in function signatures.
  • Attach lifetime parameters to structures that hold references.
  • Remember the three lifetime elision rules and know when omission is not allowed.

Key Concepts

1) Why lifetimes are needed

fn longest(a: &str, b: &str) -> &str { /* ... */ } // ❌ Does not compile

The compiler cannot know whether a returned reference belongs to a or b. If the lifetime of one of the inputs ends after the call, the returned reference could become dangling. Therefore, lifetimes are specified.

fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { ... }

'a is a label that refers to "the common valid period of the references that this function handles."

2) Lifetimes in function signatures

  • Declare lifetime parameters using <'a>.
  • If the same label is attached to both input and output references, the "output lifetime is at least the intersection of the input lifetimes."

3) Lifetimes in structures

A struct that has a reference as a field must have a lifetime parameter.

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

A Excerpt instance cannot live longer than the value part points to.

4) Three Rules for Omitting Lifetimes

# Rule
1 Each input reference is assigned its own lifetime.
2 If there is exactly one input reference, that lifetime is automatically applied to the output.
3 If the first input is &self / &mut self, that lifetime is automatically applied to the output.

If the output lifetime cannot be determined by these three rules, the compiler requires an explicit annotation.

5) Special Lifetime 'static

'static represents a reference that lives until the program ends. The type of string literals ("hello") is &'static str.

Examples

Example 1 — ex01_longest.rs: Explicit Lifetime

//Example 9-1: Function with explicit lifetime 'a
//
//The return lifetime of a function that returns one of two slices
//must be "as long as both are alive."

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

fn main() {
    let s1 = String::from("long");
    let s2 = String::from("short");
    let result = longest(&s1, &s2);
    println!("Long one: {result}");

    //When the scopes of the two inputs differ
    let s_outer = String::from("outer scope");
    {
        let s_inner = String::from("inner scope-long");
        let r = longest(&s_outer, &s_inner);
        println!("Long one (inside scope): {r}");
        //r is also valid within the lifetime of s_inner
    }
}

Example 2 — ex02_struct.rs: Struct Holding a Reference

//Example 9-2: Structures with References and Lifetimes
//
//Structures that have references in their fields require lifetime parameters.
//A structure instance cannot live longer than the values its fields point to.

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

impl<'a> Excerpt<'a> {
    fn announce(&self, prefix: &str) -> &str {
        println!("[{prefix}] Excerpt preview: {}", self.part);
        self.part
    }
}

fn main() {
    let article = String::from("In Rust, a lifetime indicates the period during which a reference is valid.");
    let first_sentence = article.split('.').next().expect("No sentence");

    let ex = Excerpt { part: first_sentence };
    let kept = ex.announce("INFO");

    println!("kept = {kept}");
    //While article is alive, both ex.part / kept are valid
}

Example 3 — ex03_elision.rs: Omission Rules

//Example 3 of Chapter 9: Lifetime Elision
//
//Frequently appearing patterns are automatically filled in by the compiler.
//Rules:
//1) Each reference input is assigned its own lifetime.
//2) If there is exactly one input, that lifetime is applied to the output.
//3) If the first argument of a method is &self / &mut self, that lifetime is applied to the output.

//(1) Can be omitted — one input reference → applied directly to output
fn first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap_or("")
}

//(2) Cannot be omitted — two inputs, so it is unclear which lifetime should be applied to output
//Explicit specification is needed.
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() >= b.len() { a } else { b }
}

//(3) Method — &self lifetime is automatically applied
struct Holder<'a> { value: &'a str }

impl<'a> Holder<'a> {
    fn value(&self) -> &str { self.value } //Lifetime omitted
}

fn main() {
    let s = String::from("Rust language is fast");
    println!("first_word = {}", first_word(&s));
    println!("longer     = {}", longer("aaa", "bb"));

    let h = Holder { value: &s };
    println!("holder     = {}", h.value());
}

Common Mistakes

Q. Does a lifetime extend the lifespan of a value?

A. No. A lifetime is just a label that tells the compiler the already determined lifespan. Declaring it does not actually increase or decrease the lifespan.

Q. Does 'a always have to be 'a?"

A. It's conventional. It's also good to give meaningful names like 'static, 'long, 'ctx. However, in simple cases, just a single alphabet is enough.

Q. It's cumbersome to write <'a> for every function.

A. If there is only one input reference like 8 cases of first_word, it is automatically handled by the omission rule. You only need to specify it where it is really necessary.

Q. Can I overuse 'static?

A. No. If forced with 'static, the caller can only pass 'static data (mostly string literals), which greatly reduces the generality of the function. Usually, a generic lifetime like 'a is used.

Summary

  • A lifetime is a label indicating the valid period of a reference — it prevents dangling references.
  • Specify with <'a> and &'a T in the function signature.
  • Structs with reference fields must have lifetime parameters.
  • In the case of a single input reference like &self, it is automatically filled by the omission rule.

Try it yourself

Solve the assignment below. The reference solution is shown below.

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/lifetimes/Cargo.tomlTOML
[package]
name = "lifetimes"
version = "0.1.0"
edition = "2021"
default-run = "lifetimes"

[dependencies]
src/lifetimes/src/main.rsRust
fn main() {
    println!("This crate has three executable examples.");
    println!();
    println!("  cargo run --bin ex01_longest");
    println!("  cargo run --bin ex02_struct");
    println!("  cargo run --bin ex03_elision");
}

Assignment

Problem 1 — Function shortest

  • File name: homework_01/src/main.rs
  • Key concept: Explicit lifetime in <'a>

Requirements

  • Implement the function shortest<'a>(a: &'a str, b: &'a str) -> &'a str.
  • Return the shorter of the two slices (if equal, return a).
  • In main, call "hello" with "hi", "Same", and "Same" and print the results.

Expected output

Short: hi
Short: same

Hint

  • if a.len() <= b.len() { a } else { b }.

Problem 2 — Struct Note

  • File name: homework_02/src/main.rs
  • Key concept: Struct with reference fields, lifetime parameters

Requirements

  • The struct Note<'a> should have two fields: title: &'a str and body: &'a str.
  • Define a method summary(&self) -> String that returns it in the "{title}: {first 20 characters}…" format.
  • If body is 20 characters or less, return as is; if longer, return the first 20 characters + ….
  • In main, create and print two Notes.

Expected output

Today's memo: short text.
Long memo: This is a longer bod…

Hint

  • body.chars().count() and body.chars().take(20).collect::<String>().

Check your answers

Try solving it yourself first, then compare your answer with the reference solution below.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

homework_01/Cargo.tomlTOML
[package]
name = "homework_01"
version = "0.1.0"
edition = "2021"

[dependencies]
homework_01/src/main.rsRust
//Key Points:
//- Since it returns one of the two input references, a lifetime annotation is necessary.
//- Both are tied to `'a` to convey the meaning "as long as both inputs are alive."

fn shortest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() <= b.len() { a } else { b }
}

fn main() {
    println!("Short version: {}", shortest("hello", "hi"));
    println!("Short version: {}", shortest("Same", "Same"));
}
homework_02/Cargo.tomlTOML
[package]
name = "homework_02"
version = "0.1.0"
edition = "2021"

[dependencies]
homework_02/src/main.rsRust
//Key Points:
//- Since the structure has reference fields, a `<'a>` lifetime parameter is necessary.
//- Cutting a string by "character units" is safe with `chars().take(N)` + `collect::<String>()`.

struct Note<'a> {
    title: &'a str,
    body: &'a str,
}

impl<'a> Note<'a> {
    fn summary(&self) -> String {
        let n = self.body.chars().count();
        if n <= 20 {
            format!("{}: {}", self.title, self.body)
        } else {
            let head: String = self.body.chars().take(20).collect();
            format!("{}: {}…", self.title, head)
        }
    }
}

fn main() {
    let short = Note { title: "Today's memo", body: "Short text." };
    let long = Note {
        title: "Long memo",
        body: "This is a longer body for testing summary cutoff.",
    };

    println!("{}", short.summary());
    println!("{}", long.summary());
}

Learn this lesson with a mentor

Work through the Rust track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now