Skip to content

Latest commit

 

History

History
148 lines (111 loc) · 3.35 KB

File metadata and controls

148 lines (111 loc) · 3.35 KB

Lifetimes

Understanding lifetimes in Rust can be tricky at first, but they are essential for ensuring memory safety without a garbage collector. Here's a beginner-friendly explanation with examples to guide you through.


🧠 Why Lifetimes?

Lifetimes are Rust’s way of ensuring that references are always valid. They help the compiler prevent dangling references and use-after-free bugs.


🧪 Life Without Lifetimes

Example: Function Taking Ownership

fn longest(a: String, b: String) -> String {
    if a.len() > b.len() {
        return a;
    } else {
        return b;
    }
}

fn main() {
    let longest_str;
    let str1 = String::from("small");
    let str2 = String::from("longer");
    longest_str = longest(str1, str2); // Ownership moved
    println!("{}", longest_str);
}

✅ This works because ownership is moved, so there's no lifetime issue.


🧬 Changing the Scope

fn longest(a: String, b: String) -> String {
    if a.len() > b.len() {
        return a;
    } else {
        return b;
    }
}

fn main() {
    let longest_str;
    let str1 = String::from("small");
    {
        let str2 = String::from("longer");
        longest_str = longest(str1, str2);
    }
    println!("{}", longest_str);
}

✅ Still valid — ownership is moved, and everything stays in scope correctly.


❌ Problem When Using References

fn longest(a: &str, b: &str) -> &str {
    if a.len() > b.len() {
        return a;
    } else {
        return b;
    }
}

fn main() {
    let longest_str;
    let str1 = String::from("small");
    {
        let str2 = String::from("longer");
        longest_str = longest(&str1, &str2);
    } // str2 goes out of scope here

    println!("{}", longest_str); // ❌ Possible dangling reference
}

❌ Compiler Error: Missing lifetime specifier

The compiler doesn’t know whether the returned reference comes from a or b, so it cannot verify the validity of the return value.


✅ Fixing It with Lifetimes

fn longest<'a>(first: &'a str, second: &'a str) -> &'a str {
    if first.len() > second.len() {
        return first;
    } else {
        return second;
    }
}

fn main() {
    let longest_str;
    let str1 = String::from("small");
    {
        let str2 = String::from("longer");
        longest_str = longest(&str1, &str2); // ❌ str2 doesn't live long enough
    }
    println!("The longest string is: {}", longest_str);
}

The 'a lifetime means:

The returned reference is valid as long as both input references are valid.

But since str2 goes out of scope early, the return value might point to invalid memory.


✅ Correct Usage: Matching Lifetimes

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

fn main() {
    let str1 = String::from("small");
    let str2 = String::from("longer");
    let result = longest(&str1, &str2);
    println!("The longest string is: {}", result);
}

✅ Both references (&str1, &str2) are valid in the same scope, so the compiler is happy.


🔑 Key Takeaways

  • Lifetimes tell the compiler how long references are valid.
  • They don’t change how long data lives, just how long references to it can be used.
  • 'a is a generic lifetime parameter.
  • The compiler uses lifetimes to prevent dangling references.