hackquest logo
M

Mir luqman

.

4

文章

3353

查看

0

关注者

0
0

Lifetimes in Rust Complete Beginner Guide

Mir luqman
2025-11-13 10:00
0
0

What is a Lifetime?

A lifetime is Rust's way of tracking how long references are valid.

Simple analogy:

Imagine you borrow a book from a library. The book has a lifetime that represents how long it exists. Your borrow also has a lifetime, which is how long you have it. You cannot keep the borrow longer than the book exists.

In Rust, the same principle applies:

  • Variables have lifetimes, spanning from when they are created to when they are destroyed.

  • References have lifetimes that determine how long they can safely point to data.

  • Rust ensures references never outlive the data they point to.


The Problem: Dangling References

What Rust prevents:

fn main() {
    let r;
    {
        let x = 5;
        r = &x;
    }
    println!("{}", r); // ERROR: x is gone, r points to nothing
}

Error:

error[E0597]: x does not live long enough

Why?

  • The variable x is destroyed at the end of the inner scope.

  • The reference r still tries to use x after it has been destroyed.

  • This would be a dangling reference, pointing to invalid memory.

  • Rust prevents this at compile time.


The Solution: Lifetimes

Rust uses lifetime annotations to track how long references are valid.

Syntax:

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

Part

Meaning

&

Reference

'a

Lifetime parameter (read as "tick a")

i32

Type

The notation 'a is like a label saying "this reference is valid for lifetime 'a".


Example 1: Function with Lifetimes

Problem without lifetimes:

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

Error:

error[E0106]: missing lifetime specifier

Why?

  • Rust does not know if the returned reference is from x or y.

  • It cannot determine how long the returned reference is valid.

Solution with lifetimes:

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

What this means:

Part

Meaning

<'a>

Declare a lifetime parameter 'a

x: &'a str

x is a reference valid for lifetime 'a

y: &'a str

y is a reference valid for lifetime 'a

-> &'a str

Return value is valid for lifetime 'a

In plain English:

"This function takes two references that are both valid for the same lifetime 'a, and returns a reference that is also valid for that lifetime."

Using it:

fn main() {
    let string1 = String::from("long string");
    let string2 = String::from("short");
    let result = longest(string1.as_str(), string2.as_str());
    println!("The longest string is: {}", result);
}

Output:

The longest string is: long string

Example 2: Lifetime Mismatch

fn main() {
    let string1 = String::from("long string");
    let result;
    {
        let string2 = String::from("short");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest is: {}", result); // ERROR
}

Error:

error[E0597]: string2 does not live long enough

Why?

  • string2 is destroyed at the end of the inner scope.

  • result might point to string2, depending on which string is longer.

  • Rust prevents using result after string2 is destroyed.

Fixed version:

fn main() {
    let string1 = String::from("long string");
    let string2 = String::from("short");
    let result = longest(string1.as_str(), string2.as_str());
    println!("The longest is: {}", result); // OK
}

Now both string1 and string2 live until the end of main, so result is safe to use.


Example 3: Structs with Lifetimes

If a struct holds references, it needs lifetime annotations.

Problem:

struct ImportantExcerpt {
    part: &str, // ERROR: missing lifetime
}

Solution:

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

What this means:

"An instance of ImportantExcerpt cannot outlive the reference it holds in part."

Using it:

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("Could not find a '.'");
    let excerpt = ImportantExcerpt {
        part: first_sentence,
    };
    println!("Excerpt: {}", excerpt.part);
}

Output:

Excerpt: Call me Ishmael

Why it works:

  • novel lives for the entire main function.

  • first_sentence is a reference to part of novel.

  • excerpt.part points to first_sentence.

  • All lifetimes are valid throughout their usage.


Lifetime Elision Rules

Rust can infer lifetimes in simple cases, so you do not always need to write them explicitly.

Rule 1: Each input reference gets its own lifetime

fn first_word(s: &str) -> &str {
    // Rust infers: fn first_word<'a>(s: &'a str) -> &'a str
    s.split_whitespace().next().unwrap()
}

Rule 2: If there is one input lifetime, it is assigned to output

fn get_first(s: &str) -> &str {
    // Rust infers: <'a>(s: &'a str) -> &'a str
    &s[0..1]
}

Rule 3: If there is &self, its lifetime is assigned to output

impl<'a> ImportantExcerpt<'a> {
    fn get_part(&self) -> &str {
        // Rust infers the lifetime from &self
        self.part
    }
}

When you MUST write lifetimes:

  • Multiple input references.

  • Structs with references.

  • Complex return types.


Common Lifetime Patterns

1. Multiple lifetimes

fn longest_with_announcement<'a, 'b>(
    x: &'a str,
    y: &'a str,
    ann: &'b str,
) -> &'a str {
    println!("Announcement: {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Meaning:

  • x and y have lifetime 'a.

  • ann has lifetime 'b, which is independent.

  • The return value has lifetime 'a.

2. Static lifetime

let s: &'static str = "I live forever";

The 'static lifetime means the reference is valid for the entire program duration.

Examples:

  • String literals like "hello"

  • Global constants


Visual Summary

┌─────────────────────────────────────────┐
│ Lifetime Problem                        │
├─────────────────────────────────────────┤
│                                         │
│ fn longest(x: &str, y: &str) -> &str   │
│                                    ↑    │
│                      Which lifetime?    │
│                                         │
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
│ Lifetime Solution                       │
├─────────────────────────────────────────┤
│                                         │
│ fn longest<'a>(                         │
│     x: &'a str,  ← same lifetime       │
│     y: &'a str,  ← same lifetime       │
│ ) -> &'a str     ← same lifetime       │
│                                         │
│ "Return is valid as long as both       │
│  x and y are valid"                    │
│                                         │
└─────────────────────────────────────────┘

Practice Exercise

Try to fix this code:

// This will not compile
fn get_first_word(s: &str) -> &str {
    let words: Vec<&str> = s.split_whitespace().collect();
    words[0]
}

Error:

error[E0515]: cannot return value referencing local variable words

Why?

  • words is created inside the function.

  • words[0] is a reference to data inside words.

  • words is destroyed when the function ends.

  • Returning words[0] would be a dangling reference.

Fixed version:

fn get_first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap()
}

Now the reference comes directly from s, not from a local variable.


Key Takeaways

Concept

Explanation

Lifetime

How long a reference is valid

'a

Lifetime parameter, similar to a generic type

Dangling reference

Reference to destroyed data that Rust prevents

Lifetime annotation

&'a T means reference valid for lifetime 'a

Elision

Rust infers lifetimes in simple cases

'static

Lives for the entire program


When Do You Need Lifetimes?

You need lifetimes when:

  • Functions return references.

  • Structs hold references.

  • Multiple input references exist.

You do NOT need lifetimes when:

  • No references involved, using owned data like String or i32.

  • Simple cases where Rust can infer them automatically.


Blockchain Development Context

In Solana development with Anchor, you will see lifetimes in account structures:

pub fn initialize<'info>(
    ctx: Context<'_, '_, '_, 'info, Initialize<'info>>
) -> Result<()> {
    // 'info is the lifetime for account data
    Ok(())
}

The 'info lifetime ensures that account references remain valid throughout the instruction execution. This becomes clearer after you have written a few Solana programs.


Conclusion

Lifetimes are Rust's way of guaranteeing memory safety at compile time. While they may seem complex at first, they prevent an entire class of bugs that plague other languages. With practice, lifetime annotations become second nature, and you will appreciate the safety they provide.

The key is to remember that lifetimes are about relationships between references, not about the data itself. They tell the compiler how long references should be valid relative to each other.

If you have questions or want more examples, feel free to discuss in the comments below.

原创
生态系统:Solana
主题:Rust
标签:
#Rust
#Lifetime
#Solana
更新于2025-11-13 11:39
0 / 1000