DEV Community

Cover image for [Rust Guide] 10.6. Lifetime Syntax and Examples
SomeB1oody
SomeB1oody

Posted on

[Rust Guide] 10.6. Lifetime Syntax and Examples

If you find this helpful, please like, bookmark, and follow. To keep learning along, follow this series.

10.6.1 Lifetime Annotation Syntax

  • Annotating lifetimes does not change how long a reference lives.
  • If a function specifies generic lifetime parameters, it can accept references with any lifetime.
  • Lifetime annotations are mainly used to describe relationships between the lifetimes of multiple references, but they do not affect lifetimes themselves.

Lifetime parameter names must start with ', are usually all lowercase, and are very short. Many developers use 'a as the lifetime parameter name.

Lifetime annotations go after the & symbol, and a space separates the annotation from the reference type.

10.6.2 Lifetime Annotation Examples

  • &i32: a plain reference
  • &'a i32: a reference with an explicit lifetime, where the referenced type is i32
  • &'a mut i32: a mutable reference with an explicit lifetime

A single lifetime annotation by itself is meaningless. The purpose of lifetime annotations is to describe the relationships between multiple generic lifetimes to Rust.

Take the code from the previous article as an example:

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<'a>(x: &'a str, y: &'a str) -> &'a str {  
    if x.len() > y.len() {  
        x  
    } else {  
        y  
    }  
}
Enter fullscreen mode Exit fullscreen mode

The lifetimes of the parameter x, the parameter y, and the return value in longest are all 'a, which means that x, y, and the return value must have the “same” lifetime.

From the example above, you can also see that when using lifetime annotations in a function signature, generic lifetime parameters must be declared inside <>. This signature tells Rust that there is a lifetime 'a, and that x, y, and the return value must live at least as long as 'a.

Because lifetime annotations are mainly used to describe relationships between the lifetimes of multiple references, but they do not affect lifetimes themselves, this writing does not change the lifetimes of the arguments. It only gives the borrow checker constraints that can be used to detect invalid calls. So the longest function does not need to know exactly how long x and y live; it only needs some scope that can stand in for 'a while satisfying the function signature’s constraints.

When a function references code outside itself, or when it is referenced by outside code, it is almost impossible to determine the lifetimes of the parameters and return values using Rust compiler alone. The lifetimes used by such a function may change from call to call. That is exactly why lifetimes sometimes need to be annotated manually.

In the example code, when we pass concrete references into the longest function, which scope is used to replace 'a? It is the overlapping part of the scopes of x and y, in other words, the shorter of the two lifetimes. And because the return value also has lifetime 'a, the returned reference remains valid in the overlap between the scopes of x and y.

That is why in the previous article and earlier in this article, the word “same” was placed in quotes: it does not mean literally identical lifetimes, but rather the overlapping part.

Next, let’s see how lifetime annotations constrain calls to longest. If we change the example above so that string1 has a different scope and string2 becomes a String, what happens?

fn main() {  
    let string1 = String::from("abcd");  
    {  
        let string2 = String::from("xyz");  
        let 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  
    }  
}
Enter fullscreen mode Exit fullscreen mode

Here, the scope of string1 is from line 2 to line 8, and the scope of string2 is from line 4 to line 7. When these are passed into longest, the function looks for the overlapping part—or, in other words, the shorter lifetime—which is the scope of string2, from line 4 to line 7. So the scope represented by 'a is from line 4 to line 7. result is valid inside the inner scope, that is, until the closing brace on line 7, so the code is still valid within 'a.

What if I change the scope of result instead?

fn main() {  
    let string1 = String::from("abcd");  
    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  
    }  
}
Enter fullscreen mode Exit fullscreen mode

In this case, the scope of string1 is from line 2 to line 9, and the scope of string2 is from line 5 to line 7. When these are passed into longest, the function looks for the overlapping part—or, in other words, the shorter lifetime—which is the scope of string2, from line 5 to line 7. So the generic lifetime parameter 'a of the function refers to the scope from line 5 to line 7, and the return value should also have that same scope. However, the result variable that receives the return value actually lives from line 3 to line 9, which exceeds the scope represented by 'a, so the program reports an error:

error[E0597]: `string2` does not live long enough
 --> src/main.rs:6:44
  |
5 |         let string2 = String::from("xyz");
  |             ------- binding `string2` declared here
6 |         result = longest(string1.as_str(), string2.as_str());
  |                                            ^^^^^^^ borrowed value does not live long enough
7 |     }
  |     - `string2` dropped here while still borrowed
8 |     println!("The longest string is {result}");
  |                                     -------- borrow later used here
Enter fullscreen mode Exit fullscreen mode

The compiler says that string2 does not live long enough. To ensure that the result printed on line 8 is valid, string2 must remain valid until the outer scope ends. Because the function parameters and return value use the same lifetime, Rust can point out this problem.

Let’s repeat the most important point from this article one more time: the actual lifetime represented by 'a is the shorter one of the two lifetimes of x and y.

Top comments (0)