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.
Lifetimes are Rust’s way of ensuring that references are always valid. They help the compiler prevent dangling references and use-after-free bugs.
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.
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.
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
}The compiler doesn’t know whether the returned reference comes from a or b, so it cannot verify the validity of the return value.
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.
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.
- 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.
'ais a generic lifetime parameter.- The compiler uses lifetimes to prevent dangling references.