Lifetimes

  • Every reference in Rust has a lifetime. Many of these lifetimes are inferred implicitly, but Rust will complain if it can’t (or if it’s inferred lifetime is wrong in some way).

  • Reference lifetimes aim to prevent dangling references:

    {
      let x;
    
      {
        let y = 15;
        x = &y;
      }
    
      // The value that x points to is out of scope here
      println!("{}", x);
    }
    
  • Rust fails to compile that snippet because it’s possible for x to reference bad memory once y goes out of scope.

  • Rust uses the borrow checker to validate reference lifetimes:

    • If a reference r refers to memory whose lifetime is shorter than its own, this can cause the scenario above.
    • This invariant is essentially what the borrow checker attempts to track.
  • Consider this function:

    fn longest(x: &str, y: &str) -> &str {
      if x.len() > y.len() { x } else { y }
    }
    
    • This code does not compile because the borrow checker doesn’t know the lifetime of the returned reference relative to the references the function receives as arguments.
    • If the returned reference is pointing to memory that x is pointing to, it xoutlive the owner of that memory, ditto for y.
  • We use lifetime annotations to tell the borrow checker that the arguments and the return value have the same lifetime:

    fn longest(x: &'a str, y: &'a str) -> &'a str {
      if x.len() > y.len() { x } else { y }
    }
    
    • The specific identifier can be any valid Rust identifier, but convention is to use 'a, then 'b, etc. Here, we’re saying that:
      • The function takes two string slices, both of which live at least as long as lifetime 'a.
      • The function returns a string slice, that lives at least as long as lifetime 'a.
      • The returned slice has a lifetime equal to the smaller of the lifetimes of the references passed in.
    • When we pass concrete references in to longest, the concrete lifetime for 'a will be the part of x's scope that overlaps with y's scope (the smaller lifetime of the two, essentially).
  • When a function returns a reference, its lifetime parameter must match one of the arguments’ lifetime parameters.

  • structs containing references require lifetime parameters as well:

    struct Foo<'a> {
      text: &'a str
    }
    
    • I’m not sure why Rust doesn’t infer this lifetime information, it seems fairly consistent.
    • Although I’m sure there are edge cases I’m not thinking about.
  • Rust does infer lifetime information in some cases following these rules (in order):

    • Each parameter/argument that is a reference gets it’s own lifetime parameter.
    • If there is exactly one input lifetime parameter, all outputs get that lifetime.
    • If one of the input parameters is &self (or &mut self), that reference’s lifetime gets assigned to all outputs.
  • These rules are not (meant to be) foolproof, but the borrow checker catches these lapses:

    struct LifetimeTest {
      name: String
    }
    
    impl LifetimeTest {
      fn announce(&self, announcement: &str) -> &str {
        announcement
      }
    }
    
    • This fails because the returned slice is assigned &self's lifetime (via rules 1 and 3 above), but actually needs &announcement's lifetime. The failure message is fairly comprehensive:
      error[E0623]: lifetime mismatch
        --> src/main.rs:81:9
         |
      80 |     fn announce(&self, announcement: &str) -> &str {
         |                                      ----     ----
         |                                      |
         |                                      this parameter and the return type are declared with different lifetimes...
      81 |         announcement
         |         ^^^^^^^^^^^^ ...but data from `announcement` is returned here
      
Edit