Original link: Understanding lifetimes in Rust
Today is a good day, and you want to try Rust programming again. The last attempt was very smooth, except for a little bit of borrowing to check , but in the process of solving the problem you understand its working mechanism, everything is worth it!
Borrowing check can't stop your Rust's grand plan. After you overcome it, you feel that writing Rust code is flying. Then you enter the compile command again and start compiling, however:
error[E0597]: `x` does not live long enough
Here again? You take a deep breath, relax, and watch this error message. "Not long enough to survive ( does not live long enough
)", what does this mean?
Introduction to life cycle
The Rust compiler uses the life cycle to keep track of whether there are any references. Reference checking is also a major responsibility of the borrowing check mechanism, which works with the life cycle mechanism to ensure that the references you use are valid.
Lifecycle annotations enable borrowing checks to determine whether the reference is valid. In most cases, the borrowing check mechanism can infer the life cycle of the reference itself, but the explicit use of life cycle annotations in the code allows the borrowing check mechanism to directly obtain information about the validity of the reference.
This article will introduce the basic concepts and usage of the life cycle, as well as some common scenarios. You need to have a certain understanding of Rust related concepts (such as borrowing checks).
Life cycle sign
One thing to note is that the Rust life cycle logo is not the same as in other languages. It is represented by adding a single quote before the variable name, usually in lowercase letters for generic representation: 'a
, 'b
and so on.
Why life cycle is needed
Why does Rust need such a strange feature? The answer lies in Rust's ownership mechanism. Borrowing check manages the allocation and release of memory, and at the same time ensures that there will be no wrong references to the released memory. Similarly, the life cycle is also checked during the compilation phase. If there are invalid references, the compilation will not pass.
In the two scenarios of function returning references and creating reference structures, the life cycle is particularly important and it is easy to make mistakes.
example
Essentially the life cycle is the scope. When the variable leaves the scope, it will be released, at this time any reference to it will become an invalid reference. Here is the simplest example copied from the official document:
// 这段代码 **无法** 通过编译
{
let x;
{ // create new scope
let y = 42;
x = &y;
} // y is dropped
println!("The value of 'x' is {}.", x);
}
This code contains two different scopes inside and outside. When the y
scope ends, 061b476b31023b is released. Even though x
is declared in the outer scope, it is still an invalid reference, and the value it references "lives" Not long enough".
In terms of life cycle, the inner and outer scopes correspond to a 'inner
and a 'outer
scope respectively. The latter is obviously longer than the former. When the 'inner
scope ends, all variables that follow its life cycle will become unavailable.
Life cycle omission
When writing a function that accepts a variable of a reference type as a parameter, the compiler can automatically deduce the life cycle of the variable in most scenarios without having to manually mark it. This situation is called "life cycle omission".
The compiler uses three rules to confirm that the function signature can omit the life cycle:
- The return value of the function is not a reference type
- There is at most one reference in the input parameter of the function
- Function is a method (
method
), that is, the first parameter is&self
or&mut self
Examples and common problems
The life cycle can easily confuse your mind, and a simple introduction to a lot of text does not necessarily make you understand how it works. The best way to understand it is of course in programming practice and in the process of solving specific problems.
Function return reference
In Rust, if a function does not receive a reference as a parameter, it cannot return a reference type, and a forced return cannot be compiled. If there is only one reference type in the function parameter, there is no need to display the life cycle of the annotation, and all the reference types in the output parameter will be regarded as having the same life cycle as the reference in the input parameter.
fn f(s: &str) -> &str {
s
}
But if you add another reference type, even if the function does not apply to it, the compilation will fail. This reflects the second rule mentioned above.
// 这段代码过不了编译
fn f(s: &str, t: &str) -> &str {
if s.len() > 5 { s } else { t }
}
When a function accepts multiple reference parameters, each parameter has its own life cycle. The compiler cannot automatically deduce which reference the function returns corresponds to. For example, which label life cycle is '???
in the following code?
// 这段代码过不了编译
fn f<'a, 'b>(s: &'a str, t: &'b str) -> &'??? str {
if s.len() > 5 { s } else { t }
}
Imagine if you want to use the reference returned by this function, what life cycle should you specify for it? Only the input parameter with the shortest life cycle can be guaranteed to be valid, and the compiler knows that they are references with the same and shorter life cycle. If, like the function above, all input parameters may be returned, then you can only ensure that their life cycles are all the same, as follows:
fn f<'a>(s: &'a str, t: &'a str) -> &'a str {
if s.len() > 5 { s } else { t }
}
If the input parameters of the function have different life cycles, but you know exactly which one you will return, you can mark the corresponding life cycle to the return type, so that the difference in the life cycle of the input parameters will not cause problems:
fn f<'a, 'b>(s: &'a str, _t: &'b str) -> &'a str {
s
}
Structure reference
The life cycle of structure references is a bit more difficult, and it is best to use non-reference types instead, so that you don't have to worry about the validity of references, the duration of the life cycle and other issues. In my experience this is usually what you want.
However, some scenarios do need to use structure references, especially when you want to write a code package that does not require data ownership transfer or data copy. Structure references can allow the original data to be accessed in other places by reference, without any troublesome processing. Data cloning problem.
For example, if you want to write code, look for the first and last two sentences of a text, and store them in a structure S
. Do not use data copy, then you need to use reference types, and annotate their life cycle:
struct S<'a> {
first: &'a str,
last: &'a str,
}
If the paragraph is empty, it will return None
. If the paragraph has only one sentence, it will return this one at the beginning and the end:
fn try_create(paragraph: &str) -> Option<S> {
let mut sentences = paragraph.split('.').filter(|s| !s.is_empty());
match (sentences.next(), sentences.next_()) {
(Some(first), Some(last)) => Some(S { first, last }),
(Some(first), None) => Some(S { first, last: first }),
_ => None,
}
}
Since this function conforms to the principle of automatic life cycle derivation (for example, the return value is not a reference type, and only receives at most one reference input parameter), there is no need to manually mark its life cycle. If you don't want to change the ownership of the original data, you really can only consider entering a reference parameter to solve the problem.
Summarize
This article only briefly introduces the life cycle in Rust. Considering its importance, I recommend reading the "Citation Validity and Life Cycle" official document to add more conceptual understanding.
If you want to advance, I recommend watching Jon Gjengset's: Crust of Rust: Lifetime Annotations . This video contains examples of multiple life cycles. Of course, there are also some life cycle introductions, which are worth watching.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。