Skip to content

Latest commit

 

History

History
186 lines (151 loc) · 4.88 KB

14_lifetimes.md

File metadata and controls

186 lines (151 loc) · 4.88 KB

Lifetimes

Lifetimes ensure that references point to a valid memory. Lifetime of a reference is as big as is the scope of a variable:

fn main() {
    let x = 13;       // --------+- 'a
    let xr = &x;      //         |
    {                 //         |
        let y = 37;   // --+- 'b |
        let yr = &y;  //   |     |
    }                 // --+     |
}                     // --------+

Lifetime denotes scope, lifetime-parameter denotes parameters that the compiler substitutes with a real lifetime, same as inferring types of generics.

Lifetime-parameters are named 'a, an apostrophe and a lowercase name, often just a single letter, notable exception being 'static. As generics, they're declared using the <> syntax:

fn gimme_bigger<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() > s2.len() {
        s1
    } else {
        s2
    }
}

In this case, references s1 and s2 may have different lifetimes, and it is impossible to determine the returned reference's lifetime at compile-time.

When a lifetime-parameter is used on multiple references, the compiler choses the shortest lifetime of all to ensure that the returned reference lives long enough:

fn gimme_bigger<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() > s2.len() {
        s1
    } else {
        s2
    }
}

fn main() {
    let s1 = "kekega".to_string();     // --------+- 'a
    let res;                           //         |
    {                                  //         |
        let s2 = "bur".to_string();    // --+- 'b |
        res = gimme_bigger(&s1, &s2);  //   |     | res has lifetime 'b
    }                                  // --+     |
    // println!("bigger is {}", res);  //         | nope 😿 'b is invalid
}                                      // --------+

The res reference has the shorter lifetime 'b. When used in the println! macro, 'b already out of scope, and so is res, the program will not compile.

Lifetime-parameters can also be used with mutable references:

  • &i32 - a reference
  • &'a i32 - a reference with a lifetime-parameterr
  • &'a mut i32 - a mutable reference with a lifetime parameter

Subtyping

Subtyping is a type of compile-time polymorphism that check whether operations working with a supertype of type T, say F<T>, can operate also on type T .

In the context of lifetimes, if lifetime 'a lives longer than lifetime 'b then 'a is a subtype of 'b.

Elision

Rust has a set of lifetime elision rules built into the compiler for cases when a function's parameter and return value's lifetime-parameters can be inferred.

The first rule states that all input references get their own lifetime-parameter:

fn print_longest<'a, 'b>(s1: &'a str, s2: &'b str) {
    println!("{}", if s1.len() > s2.len() { s1 } else { s2 });
}

// elides to
fn print_longest(s1: &str, s2: &str) {
    println!("{}", if s1.len() > s2.len() { s1 } else { s2 });
}

The second rule is that when the function takes a single reference as an argument and returns references, the returned references' lifetime is the same as the input reference's lifetime:

fn first_half<'a>(s: &'a str) -> &'a str {
    let h = s.len() / 2;
    &s[..h]
}

// elides to
fn first_half(s: &str) -> &str {
    let h = s.len() / 2;
    &s[..h]
}

The third rule states that when a method that takes &self returns references, the returned references' lifetime is the same as the lifetime of &self, even if it also takes other references:

struct Text<'a> {
    text: &'a str,
}

impl<'a> Text<'a> {
    fn print_other_get<'b>(self: &'a Self, s: &'b str) -> &'a str {
        println!("other is {}", s);
        self.text
    }
    // elides to
    fn print_other_get(&self, s: &str) -> &str {
        println!("other is {}", s);
        self.text
    }
}

This works only if it returns a reference from &self. Returning a different reference needs explicit lifetime-parameters:

struct Text<'a> {
    text: &'a str,
}

impl<'a> Text<'a> {
    fn or_longer(&self, s: &'a str) -> &'a str {
        if self.text.len() > s.len() {
            self.text
        } else {
            s
        }
    }
}

Static

The 'static lifetime is the subtype of all other lifetimes — it lives for the entire duration of the program.

The most notable example is string literals, whose full type is &'static str:

fn gimme_bigger<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() > s2.len() {
        s1
    } else {
        s2
    }
}

fn main() {
    let s1 = "kekega";                 // &'static str
    let res;
    {
        let s2 = "bur";                // &'static str
        res = gimme_bigger(&s1, &s2);  // res has lifetime 'static
    }
    println!("bigger is {}", res);     // ok 🎉
}

Other references 'static lifetimes are ones created in the global scope declared as static.