What is lifetime in Rust

ClickUp
Note
AI Status
Last Edit By
Last edited time
Jul 28, 2023 02:58 PM
Metatag
Slug
what-is-lifetime-in-rust
Writer
Published
Published
Date
Jul 28, 2023
Category
Rust
One of the peculiarities of Rust that you will encounter or have already encountered many times is lifetime.
This is also true whether you are a junior, medium, or senior engineer. Lifetimes in Rust is like a math problem that you need to solve quickly but takes three semesters to understand.
It's not quite the right analogy, though.
Whatever, you will always have to deal with it for the rest of your Rusty journey.

What is Lifetime in Rust?

While the Rust borrow checker's main job is to ensure that you follow the borrowing rules to ensure valid references, lifetime helps you identify how long references are valid. The two work together to ensure that your program never has invalid references.
In this article, you will learn about why you need lifetime, lifetime annotation, lifetime elision, and compiler lifetime rules.
Keep reading!

Why do you need Lifetime?

With lifetime, you can help the borrower checker identify your program's reference intention.
Suppose you have a variable that needs to live outside its scope; you don’t notice it at first, but the borrow checker can. Although it cannot decide how long the variable will exist, it is up to you to decide, and the borrow checker will help you calculate whether it is correct or not.
let x; { let y = 23; x = &y; } // y already dropped println!("Value of x is {:?}", x); // invalid! <<----------------<

Lifetime annotation

You have heard of annotations in programming languages such as Java or Kotlin. In Rust's lifetime, it is quite different. You will learn something new.

How to write lifetime annotation in Rust?

In the above code, you will need to add a lifetime annotation. You write an annotation with a leading apostrophe followed by a variable name.
'yosh 'a 'X
Yes, you can use a long word for the lifetime variable name. However, usually you just need a single, lowercase letter. It is easier for your team if you follow the example of others by starting your lifetime variable names with 'a, 'b, and so on.

Lifetime Elision

When you know about lifetime annotation, you will write it a lot in source code. But actually, you should not.
Lifetime elision is a Rust feature that allows you to omit explicit lifetime annotation when it can be inferred by the borrow checker. This feature aims to make you write more concise code by reducing explicit lifetime annotations.
The example code that requires lifetime annotation is like this, similar to what you will find in Rust Borrow Checker:
fn getLastChar<'a>(input: &'a str) -> &'a str { &input[input.len() - 1..] }
In the above code, the lifetime 'a states that the input output must live as long as the input.
But actually, you can just write it like this.
fn getLastChar(input: &str) -> &str { &input[input.len() - 1..] }
Why? It's because the compiler infers lifetime.
Curious? Keep reading.

How does the compiler infer lifetime?

To figure out references lifetimes, compiler has three rules.
These rules apply to fn as well as impl blocks.
In a function, there is input and output. The first rule applies to input lifetimes; the second and third rules apply to output lifetimes.
If, even after applying these rules, there is still ambiguity, the compiler will halt and report an error.
Let’s try to apply these rules to this function
fn longestString(x: &str, y: &str) -> &str

The first rule: Assign each reference parameter a different lifetime

When the compiler finds a parameter that is a reference, it will assign a lifetime. In longestString code, you see that x and y are reference parameters of type &str.
fn longestString<'a, 'b>(x: &'a str, y: &'b str) -> &str
The lifetime 'a is assigned to the x parameter. While 'b is assigned to the y parameter.
This rule only applies to input; hence, output has no lifetime yet.

The second rule: If only one input lifetime parameter, the output uses same lifetime

Suppose the longestString function has only one parameter, x. Then the output will use lifetime 'a like the input.
fn longestString<'a>(x: &'a str) -> &'a str
Now the output has a lifetime, because this rule assigns the output a lifetime based on the input lifetime. Only if there is only one input lifetime.

The third rule: If there is &self or &mut self, the output uses self lifetime

In this case, longestString is a method because it has the &self parameter.
fn longestString<'a, 'b>(&'a self, x: &'b str) -> &'a str
You will find this rule when you implement the method.

Ambiguous Lifetimes

In the first rule above, you see that the rule doesn’t assign the output reference a lifetime yet. The compiler then tries to assign a lifetime based on the second rule. But it turns out that the function fn longestString(x: &str , y: &str) -> &str is invalid for the second rule.
This is when the compiler reports an error. You must assign lifetimes manually.

Conclusion

Lifetimes provides the developer with a mechanism to help the compiler and the borrow checker know how long a reference will live before it is dropped. The compiler already has rules to avoid explicitly writing lifetime annotation, but when you must write it, the three rules can help you understand where the compiler is confused.
Overall, understanding lifetime helps you write safe and efficient Rust code.
 
 

Discussion (0)

Related Posts