DEV Community

Cover image for [RustGuide] 10.8. Lifetime Annotations in Method Definitions and Static Lifetime
SomeB1oody
SomeB1oody

Posted on

[RustGuide] 10.8. Lifetime Annotations in Method Definitions and Static Lifetime

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

10.8.1 Lifetime Annotations in Method Definitions

Do you still remember the three lifetime elision rules mentioned in the previous article, 10.7. Input and Output Lifetimes and the 3 Rules?

Rule 1: Each reference parameter gets its own lifetime. A single-parameter function has one lifetime, a two-parameter function has two lifetimes, and so on.

Rule 2: If there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters. In other words, if there is only one input lifetime, that lifetime is the lifetime of every possible return value of the function.

Rule 3: If there are multiple input lifetime parameters, but one of them is &self or &mut self (that is, the function is a method), then the lifetime of self is assigned to all output lifetime parameters.

In the example from the previous article, we applied Rules 1 and 2, but not Rule 3, because Rule 3 applies only to methods. So here we will talk about Rule 3, which is lifetime annotations in method definitions.

A method needs a struct, and using lifetimes on a struct when defining methods works the same way as generic parameters do (see 10.7. Input and Output Lifetimes and the 3 Rules).

Where a lifetime parameter is declared and used depends on whether the lifetime parameter is related to fields, method parameters, or return values.

Lifetime names for struct fields are always declared after the impl keyword and then used after the struct name, because these lifetimes are part of the struct type itself.

Inside method signatures in an impl block, references must be tied to the lifetime of the struct field reference, or they can also be independent. In addition, lifetime elision rules often make lifetime annotations unnecessary in methods.

Enough talk—let’s look at an example:

struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}
Enter fullscreen mode Exit fullscreen mode

First, the ImportantExcerpt struct is defined, and then the level method is defined for it. The level method takes only &self as a parameter, and its return value is i32, so it does not reference anything.

The phrase “lifetime names for struct fields are always declared after the impl keyword and then used after the struct name” refers to the fact that line 4 writes <'a> after impl, and <'a> is also written after the struct name ImportantExcerpt.

Note that neither of the two <'a> annotations on line 4 can be omitted, but the level function does not need a lifetime annotation on &self because lifetime elision Rules 1 and 2 apply.

Now add another method:

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

According to lifetime elision Rule 1, the &self and announcement parameters each receive a lifetime:

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

According to lifetime elision Rule 3, the return value is assigned the same lifetime as &self:

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

At this point, all lifetimes have been inferred, so the compiler can compile the code successfully.

10.8.2 The 'static Lifetime

Rust has a special lifetime called 'static, which means the entire duration of the program, or the whole execution time of the program.

For example, all string literals have the 'static lifetime, such as:

let s: &'static str = "I have a static lifetime.";
Enter fullscreen mode Exit fullscreen mode

This is a string literal, so it can be annotated with 'static.

The reason string literals have the 'static lifetime is that they are stored directly in the binary file and placed in static memory at runtime, so they are always available.

Before assigning 'static to an ordinary reference—which the compiler often suggests when it reports an error—you must think carefully: do you really need this reference to live for the entire duration of the program? Most likely, the compiler error appears because of a dangling reference or a lifetime mismatch. At that point, you should try to solve those problems instead of simply slapping a 'static lifetime on it.

10.8.3 Generic Type Parameters, Trait Bounds, and Lifetimes

Finally, let’s look at an example that uses generic type parameters, trait bounds, and lifetimes at the same time:

use std::fmt::Display;

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
    }
}
Enter fullscreen mode Exit fullscreen mode

The purpose of this function is to return the longer of the two string slices x and y, but it now has one more parameter, ann, which stands for announcement. Its type is the generic type T, and according to the constraint in where, T can be replaced by any type that implements the Display trait.

Top comments (0)