First of all what are lifetimes? Lifetimes define the valid range of a reference, in other words lifetimes are a tool used by the compiler to compare the lifetime of the owner and the borrower, in order to avoid dangling pointer as much as possible.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fn main() {
    {
        let r;
        {
            let x = 5;
            r = &x;
            // ^^ borrowed value does not live long enough
        }
        // - `x` dropped here while still borrowed
        println!("r: {}", r);
        // - borrow later used here
    }
}

let r; claims a variable that becomes a reference to the x variable in the inner statement block, and when the inner statement block ends, the variable x (owner) is released out of scope and r becomes a dangling reference, so the compiler reports an error.

Borrowing checker

rust Borrowing checker

The compiler has a borrow checker to compare scopes and decide whether the reference is valid. The above diagram has two comments 'a and 'b to represent the scope of r, x respectively. The 'b of the memory statement block is much smaller than the outer 'a, and the compilation stage finds that x has a shorter life cycle than r, so an error prevents compilation.

rust Borrowing checker

If you want to fix it, it’s as simple as putting println in the inner statement block. Most of the time, we don’t need to display the specified lifetimes, the compiler is smart enough to infer them for us automatically, but there are exceptions.

Look at an example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Let’s look at an example where we need to specify lifetimes, longest returns the longest reference to a string and compiles with an error.

rust compiles with an erro

The compiler is baffled, he doesn’t know which reference is returned by the function and needs to specify the life cycle. And it was very kind to give a hint.

1
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str

Here is a rule of lifetimes: If the function input parameters have references (all references must have lifetimes), if the return result is not a reference, then you can omit to mark the life cycle, and vice versa must be marked.

The reason is very simple, if the return result is a reference, then according to the three principles of ownership, the object he refers to must not be created inside the function, because the function returns, the reference to the object will be released, the return of the reference becomes a dangling reference.

Therefore, the life cycle of the returned reference must be consistent with the input parameters, which leads to the second rule of lifetimes: If the input parameters are only one reference with lifetimes, and the return value is also a reference, then the two life cycles must be consistent, you can omit to mark lifetimes, and vice versa must be marked.

Here a little around, you need to think carefully and run more test examples to deepen understanding, I just started to contact here also took a lot of detours.

Life cycle syntax

1
2
3
&i32        // a reference
&'a i32     // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime

The syntax is nothing special, it is a generic syntax, usually starting from 'a, 'b, 'c, and so on, or you can write it as something else.

1
2
3
4
5
6
7
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

The above example <'a> is a generic syntax and the function signature indicates that the x, y life cycles are the same, so the return reference is naturally 'a.

1
2
3
fn main() {
  let s: &'static str = "I have a static lifetime.";
}

But the static lifecycle above uses the `‘static’’ keyword to indicate that the reference survives for the entire duration of the program. The string will be compiled into the binary data segment.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
    where T: Display
{
    println!("Announcement! {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

The above is an example combined with normal generic parameters, <'a, T>, where the first 'a is the lifecycle and the second is the generic T, requiring the implementation of the Display trait. Note that the order should not be reversed here, as an error will be reported if T is placed before it.

1
2
3
4
5
6
7
error: lifetime parameters must be declared prior to type parameters
 --> src/main.rs:6:36
  |
6 | fn longest_with_an_announcement<T, 'a>(x: &'a str, y: &'a str, ann: T) -> &'a str
  |                                ----^^- help: reorder the parameters: lifetimes, then types: `<'a, T>`

error: aborting due to previous error

Lifetime Annotations in Function Signatures

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {}", result);
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

rust

This example will report an error, even though we specified the life cycle, but it doesn’t help. string2 is released at the end of the block, and it is illegal to continue using the string2 reference when println resut. Just put println inside the block.

Multiple lifecycle parameters

1
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &'a str

If the function has more than one lifecycle parameter, 'a , 'b , and returns a reference to 'a , the compiler will report an error.

1
2
3
4
5
6
7
11 | fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
   |                                   -------     -------
   |                                   |
   |                                   this parameter and the return type are declared with different lifetimes...
...
15 |       y
   |       ^ ...but data from `y` is returned here

The principle is simple: the compiler cannot determine the valid length of the two lifetimes and needs to specify a constraint.

1
2
3
4
5
6
7
fn longest<'a, 'b: 'a>(x: &'a str, y: &'b str) -> &'a str {
    if x.len() > y.len() {
      x
    } else {
      y
    }
}

The final function is shown above, where 'b: 'a means that 'b must include the 'a life cycle length.

annotations within the structure

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.')
        .next()
        .expect("Could not find a '.'");
    let i = ImportantExcerpt { part: first_sentence };
}

When a structure is defined, if a reference exists, it is also annotated with 'a, and the generic syntax is not repeated here. part is a string reference that exists before the instance i is created, and is released at the same time as i leaves scope.

If you remove the lifetime annotation from the generic, you will get an error.

1
2
3
4
5
6
7
error[E0106]: missing lifetime specifier
 --> src/main.rs:2:11
  |
2 |     part: & str,
  |           ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter

Annotation of struct methods

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

The annotation syntax for struct methods is to write <'a> after the impl keyword, and use it after the struct name. This example is not annotated with announce_and_return_part, which leads to another rule If the method has multiple input lifecycle parameters and one of the parameters is &self or &mut self, which means it is a method of an object, then all output lifecycle parameters are given to self’s life cycle

If you replace the announce_and_return_part return value with announcement you will get an error.

1
2
3
4
5
6
7
9  |     fn announce_and_return_part(&self, announcement: &str) -> &str {
   |                                                      ----     ----
   |                                                      |
   |this parameter and the return type are declared with different lifetimes...
...
12 |announcement
   |         ^^^^^^^^^^^^ ...but data from `announcement` is returned heres

This requires a display specification to assist the compiler in completing the check and specifying lifetime. In addition, the life cycle is more complicated when subtyping, covariance, and inversion are involved, for those interested, see the nomicon official documentation.

Summary

I’ve written a bunch of miscellaneous things, but I think we should get started and practice more, and figure it out. When I first learned rust, I was told to add 'a' to the life cycle if I couldn’t compile it, and to use clone for data.

In fact, or to understand the essence, and Go GC runtime traversal is different, the inspector to determine when and where to release resources at compile time, you need to collect additional information. For example, a structure object has a reference field. If its lifecycle cannot be confirmed, should the field be released when the structure object is released? Or can the field be automatically released early, so does it result in a dangling reference? Obviously, this violates the security rules.