| title | Lifetimes |
|---|---|
| slug | lifetimes |
[!recap]
- Ownership: Each piece of data has a single owner, and data is only scoped to its owner (unless it is borrowed).
- In Rust, borrowing is the act of creating a reference to a value without copying the data and without taking ownership (move). While a reference exists, the original owner retains ownership, but its access to the data is restricted (or completely locked if
&mut) until that reference’s last use.- Liveness:
- Owned values (
T) are dropped at the end of their scope (unless moved early).- References (
&T,&mut T) effectively end/ expire at their last use. This is known as Non-Lexical Lifetimes (NLL).- Lifetimes are Rust's way of guaranteeing that a reference is valid within a discrete region of code at compile time.
- A lifetime is a construct the Rust compiler's borrow checker uses to track how long references remain valid and every reference in Rust has a lifetime.
- However, most local lifetimes are implicitly figured out by the compiler through a mechanism called lifetime elision.
- The primary purpose of lifetimes is to prevent dangling references, ensuring data is never dropped while a pointer still looks at it.
Annotating a lifetime does not extend the lifespan of an actual value. It is simply a contract that tells the compiler, "The lifespan of this reference is guaranteed to be tied to the lifespan of this data."
Lifetimes are declared with a single leading apostrophe '. By convention, lowercase letters are used starting from 'a, moving alphabetically if multiple lifetimes are required. 'static is a special lifetime that indicating the reference can live for the entire duration of the program execution.
fn main() {
let a = "A";
take_str(a);
take_str_a(a);
take_str_static(a);
}
fn take_str(s: &str) {
println!("{s}");
}
fn take_str_a<'a>(s: &'a str) {
println!("{s}");
}
fn take_str_static(s: &'static str) {
println!("{s}");
}Prior to Rust 1.0, around 2014, lifetime elision did not exist. We had to explicitly type out 'a and 'b annotations for every single reference, even in the simplest functions. The Rust community introduced lifetime elisions to automatically infer common patterns, mainly in function signatures from Rust 1.0. However, at that time, lifetimes remained rigidly tied to the lexical scope (reference lasted until the closing curly brace) of the code block. Non-Lexical Lifetimes(NLL) were introduced with Rust 2018, allowing references to expire as soon as they are last used instead of end of lexical scope. In Rust 2024, the language expanded support for Return Position Impl Trait (RPIT) functions (such as fn foo() -> impl Trait) which we had to explicitly mentioned in their bounds (+ 'a or + '_). To prevent over-capturing Rust also introduced precise capturing syntax (+ use<>).
[!tip] Elision Rules
- Each elided lifetime in input position becomes a distinct lifetime parameter.
- If there is exactly one input lifetime position (elided or not), that lifetime is assigned to all elided output lifetimes.
- If there are multiple input lifetime positions, but one of them is
&selfor&mut self, the lifetime ofselfis assigned to all elided output lifetimes.- Otherwise, it is an error to elide an output lifetime.
These rules are still valid. But now, lifetime elision has expanded beyond function signatures. So, let's go through examples to identify where we can elide lifetimes and where we must explicitly add them.
Tip
- Add lifetimes after the
&sign to input and/or output references.(x: &str)→(x: &'a str),(x: &mut str)→(x: &'a mut str)
- After the function name, mention the lifetimes like generic types, unless
'static.fn foo()→fn foo<'a>()orfn foo<'a, 'b>()
fn main() {
let a = "A";
take_str(a);
}
fn take_str(s: &str) {
println!("{s}");
}
// take_str<'a>(s: &'a str){}
// take_str(s: &'static str){} ⭐️ MUST ANNOTATE
fn main() {
let a = return_str();
let b = return_str_static();
println!("{a} {b}");
}
fn return_str<'a>() -> &'a str {
"A"
}
fn return_str_static() -> &'static str {
"B"
}fn main() {
let a = "A";
let b = take_and_return_str(a);
println!("{a} {b}");
}
fn take_and_return_str(s: &str) -> &str {
println!("{s}");
"B"
}
// take_and_return_str<'a>(s: &'a str) -> &'a str{}
// take_and_return_str(s: &'static str) -> &'static str{}fn main() {
let (a, b) = ("A", "B");
take_strs(a, b);
}
fn take_strs(a: &str, b: &str) {
println!("{a} {b}");
}
// take_strs<'a>(a: &'a str, b: &'a str) {} 💡both input share same lifetime
// take_strs<'a, 'b>(a: &'a str, b: &'b str){} 💡each input having different lifetimes⭐️ MUST ANNOTATE
fn main() {
let (a, b) = ("A", "B");
let c = take_strs_return_str(a, b);
println!("{c}")
}
fn take_strs_return_str<'a>(a: &'a str, b: &'a str) -> &'a str { // 💡both input, output share same lifetime
println!("{a} {b}");
b
}
// OR take_strs_return_str<'a, 'b>(a: &'a str, b: &'b str) -> &'b str{} // 💡each input having different lifetimes⭐️ MUST ANNOTATE
fn main() {
let (a, b) = ("A", "B");
let (c, d) = take_strs_return_strs(a, b);
println!("{c} {d}")
}
fn take_strs_return_strs<'a>(a: &'a str, b: &'a str) -> (&'a str, &'a str) {
println!("{a} {b}");
(b, a)
}
// OR take_strs_return_strs<'a, 'b>(a: &'a str, b: &'b str) -> (&'b str, &'a str){}⭐️ MUST ANNOTATE
Tip
- Add lifetimes after the
&sign to element references.x: &str→x: &'a str,x: &mut str→x: &'a mut str
- After the name of the struct or enum, mention the lifetimes like generic types, unless
'static.struct Person→struct Person<'a>orstruct Person<'a, 'b>enum Team→enum Team<'a>orenum Team<'a, 'b>
- In the
implblock,- Either anonymous lifetimes:
impl Person→impl Person<'_>orimpl Person<'_, '_>, when methods inside the impl block only take&selfor&mut selfand return types that don't need to match the struct's inner lifetime. - Or named lifetimes:
impl Person→impl<'a> Person<'a>orimpl<'a, 'b> Person<'a, 'b>
- Either anonymous lifetimes:
fn main() {
let steve = Person::new("Steve", "Jobs");
steve.intro();
}
struct Person<'a> {
fname: &'a str,
lname: &'a str,
}
impl<'a> Person<'a> {
fn new(fname: &'a str, lname: &'a str) -> Self {
Self { fname, lname }
}
}
impl Person<'_> {
fn intro(&self) {
println!("Hello! I am {} {}.", self.fname, self.lname)
}
}
// 💡 fn intro(&self) {} can be moved to first impl block toofn main() {
let steve = Person::new("Steve", "Jobs");
steve.intro();
}
struct Person {
fname: &'static str,
lname: &'static str,
}
impl Person {
fn new(fname: &'static str, lname: &'static str) -> Self {
Self { fname, lname }
}
fn intro(&self) {
println!("Hello! I am {} {}.", self.fname, self.lname)
}
}fn main() {
let steve = Person::new("Steve", "Jobs");
steve.intro();
}
struct Person<'a, 'b> {
fname: &'a str,
lname: &'b str,
}
impl<'a, 'b> Person<'a, 'b> {
fn new(fname: &'a str, lname: &'b str) -> Self {
Self { fname, lname }
}
}
impl Person<'_, '_> {
fn intro(&self) {
println!("Hello! I am {} {}.", self.fname, self.lname)
}
}
// 💡 fn intro(&self) {} can be moved to first impl block toofn main() {
let steve = Person { name: "Steve" };
let has_steve = steve.has_name("Steve");
println!("{has_steve}");
}
struct Person<'a> {
name: &'a str,
}
trait HasName {
fn has_name(&self, name: &str) -> bool;
}
impl HasName for Person<'_> {
fn has_name(&self, name: &str) -> bool {
self.name == name
}
}fn main() {
let steve = Person { name: "Steve" };
let longer_name = steve.longer_name("Jobs");
println!("{longer_name}");
}
struct Person<'a> {
name: &'a str,
}
trait LongerName<'a> {
fn longer_name(&self, name: &'a str) -> &'a str;
}
impl<'a> LongerName<'a> for Person<'a> {
fn longer_name(&self, name: &'a str) -> &'a str {
if self.name.len() >= name.len() {
self.name
} else {
name
}
}
}
// Person.name and the longer_name method input/return values use the same lifetime identifierfn main() {
let steve = Person { name: "Steve" };
let longer_name = steve.longer_name("Jobs");
println!("{longer_name}");
}
struct Person<'p> {
name: &'p str,
}
trait LongerName<'n> {
fn longer_name(&self, name: &'n str) -> &'n str;
}
impl<'p, 'n> LongerName<'n> for Person<'p>
where
'p: 'n,
{
fn longer_name(&self, name: &'n str) -> &'n str {
if self.name.len() >= name.len() {
self.name
} else {
name
}
}
}
// impl<'p: 'n, 'n> LongerName<'n> for Person<'p> {}