Skip to content

Latest commit

 

History

History
executable file
·
356 lines (275 loc) · 9.42 KB

File metadata and controls

executable file
·
356 lines (275 loc) · 9.42 KB
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.

Lifetimes

  • 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}");
}

Elisions

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 &self or &mut self, the lifetime of self is 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.

On Function Declarations

Tip

  1. Add lifetimes after the & sign to input and/or output references.
    • (x: &str)(x: &'a str), (x: &mut str)(x: &'a mut str)
  2. After the function name, mention the lifetimes like generic types, unless 'static.
    • fn foo()fn foo<'a>() or fn foo<'a, 'b>()

Single Input

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){} 

Single Output

⭐️ 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"
}

Single Input-Output

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{}

Multiple Input

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

Multiple Input - Single Output

⭐️ 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

Multiple Input-Output

⭐️ 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){}

On Struct, Enum Definitions

⭐️ MUST ANNOTATE

Tip

  1. Add lifetimes after the & sign to element references.
    • x: &strx: &'a str, x: &mut strx: &'a mut str
  2. After the name of the struct or enum, mention the lifetimes like generic types, unless 'static.
    • struct Personstruct Person<'a> or struct Person<'a, 'b>
    • enum Teamenum Team<'a> or enum Team<'a, 'b>
  3. In the impl block,
    • Either anonymous lifetimes: impl Personimpl Person<'_> or impl Person<'_, '_> , when methods inside the impl block only take &self or &mut self and return types that don't need to match the struct's inner lifetime.
    • Or named lifetimes: impl Personimpl<'a> Person<'a> or impl<'a, 'b> Person<'a, 'b>

Single Lifetime

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 too

Static Lifetime

fn 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)
    }
}

Multiple Lifetimes

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 too

On Trait Implementations

With Anonymous Lifetime

fn 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
    }
}

With Named Lifetimes

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 identifier

With Separate Named Lifetimes

fn 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> {}