1. The role of life cycle: prevent dangling references
A dangling reference is when the data pointed to by the reference has been released, causing the reference to become invalid. Rust captures such problems at compile time through lifecycle and borrow checkers, thus avoiding runtime errors.
Consider the following example (Listing 10-16), the code tries to transfer external variablesr
Set to reference inner variablesx
but the inner variable is cleaned after the scope ends, thus making the referencer
Point to released data:
fn main() { let r; // The scope of r extends to the entire main function { let x = 5; r = &x; // Set r to the reference of x, at this time the life cycle of x is only within this block } // Here x has been out of scope, r will become a dangling reference println!("r: {}", r); }
The compiler will report an error, promptx does not live long enough
, this is becausex
Life cycle ratior
Short, no guaranteer
The referenced memory is always valid.
2. Borrowing Inspector and Lifecycle Annotations
Rust'sBorrowing InspectorResponsible for comparing the scope (life cycle) of variables to ensure that all references are valid when used. We can clearly tell the compiler the valid range of each reference by manually annotating the life cycle in the code.
For example, below (Listing 10-17) we use'a
and'b
Marked separatelyr
andx
Life cycle:
fn main() { // 'a: r's life cycle extends to the entire main let r: &'a i32; { // 'b: x's life cycle is only within this block let x = 5; r = &x; } // At this time, the life cycle of x referenced by r has ended, and the compiler will report an error println!("r: {}", r); }
In this code, the borrow checker compares the life cycle'a
and'b
,Discoverr
The life cycle (outer layer) is greater than what it refers tox
The life cycle (inner layer) is long, so the compilation is refused, thus preventing the dangling reference problem.
To fix this error, we need to make sure that the lifecycle of the reference does not exceed the lifecycle of the data itself.
For example,x
The statement moved tor
Within the scope of the system, make sure it is valid throughout the use period:
fn main() { let x = 5; // The life cycle of x extends to main let r = &x; // r quotes x, the life cycle is consistent with x println!("r: {}", r); }
This way the compiler can confirmr
The referenced memory is always valid.
3. Use generic lifecycle in functions
Consider a function that returns a longer string slicelongest
. Since function parameters are references, in order to ensure that the return value reference is valid, the life cycle must be specified for the reference. It may initially be written as the following code, but an error will be reported during compilation:
fn longest(x: &str, y: &str) -> &str { if () > () { x } else { y } }
The compiler does not know if the returned reference belongs tox
stilly
The life cycle of the , so the error was reported. The solution is to add the same lifecycle parameter to the reference in the function signature as shown below (Listing 10-21):
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if () > () { x } else { y } }
This means for a certain life cycle'a
,x
andy
The quote is at least active'a
time, and the returned reference is guaranteed to be at least active'a
. When we calllongest
When the compiler selectsx
andy
The shorter life cycle in the period is used as the actual life cycle of the reference.
For example, in the following code,string1
The life cycle is longer thanstring2
life cycle, so the life cycle of the returned reference is shorterstring2
scope of the scope (Listing 10-22):
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); // Here, result references string2 } // If you use result here, an error occurs because string2 is out of scope}
This ensures the security of references: the compiler refuses to use references when the data is invalid.
4. More applications of life cycle annotations
4.1 Use life cycle in structures
If the structure holds a reference, you need to specify a lifecycle parameter for the reference field in the structure definition.
For example, define a structure that holds a string sliceImportantExcerpt
(Listing 10-24):
struct ImportantExcerpt<'a> { part: &'a str, } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = ('.').next().expect("Could not find a '.'"); let excerpt = ImportantExcerpt { part: first_sentence }; println!("Excerpt: {}", ); }
Here, we declare the life cycle parameters after the structure name'a
and use it in fieldspart
on the reference type. This meansImportantExcerpt
An instance cannot live longer than the reference it holds.
4.2 Life cycle omission rules
Rust designed a setLife cycle omission rules, in most cases, there is no need to explicitly label life cycles. For example, functionsfirst_word
(Listing 10-25) Can still compile without explicit annotations:
fn first_word(s: &str) -> &str { let bytes = s.as_bytes(); for (i, &item) in ().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] }
The compiler automatically infers that all references should share the same life cycle based on the rules. However, if the function involves multiple references and the relationship is complex, you may need to manually add lifecycle annotations.
4.3 Static life cycle
'static
is a special life cycle that means that references can be valid throughout the entire program execution. All string literals have'static
life cycle:
let s: &'static str = "I have a static lifetime.";
Usually we don't need to use it explicitly'static
, unless you encounter compiler suggestions or special needs. In most scenarios, the correct use of lifecycle parameters can meet memory security needs.
5. Comprehensive use of generics, traits and life cycles
Since lifecycles are also a generic, they can be used with type generics and trait constraints.
For example, here is an extended versionlongest
Function, which not only returns a longer string slice, but also accepts an extra parameterann
, this parameter is required to be implementedDisplay
Features (Listing 10-11 Comprehensive Example):
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 } else { y } } fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest_with_an_announcement(string1.as_str(), string2, "Comparing strings"); println!("The longest string is {}", result); }
In this example, we declare the lifecycle parameters in the angle brackets after the function name.'a
and generic type parametersT
。T
passwhere
The clause is constrained to be implementedDisplay
, ensure that we can use it{}
Format outputann
. At the same time, the life cycle of the returned reference is'a
, make sure it is consistent with the shorter life cycle in the input parameters.
Summarize
This article describes how to verify the validity of a reference with lifecycle annotations in Rust and ensure that the reference does not have drape problems. We discussed:
- Prevent dangling references: How to leverage lifecycle to ensure that references do not exceed the scope of the data it points to, and how the borrow checker analyzes the lifecycle at compile time.
-
Generic life cycle in a function: Add life cycle parameters to function parameters and return values (such as
'a
), which enables the function to return references safely, as shown in the examplelongest
Correct way to write functions. - Life cycle omission rules: Explain how Rust automatically infers the lifecycle of a reference in a simple case, thus making the code more concise.
- Use life cycles in structures: When a structure holds a reference, it is necessary to specify a life cycle for the reference field to ensure that the structure instance will not exceed the valid range of the data it references.
-
Static life cycle and comprehensive application: Introduction
'static
Lifecycle and how to combine lifecycle with generics and trait constraints to write more flexible code.
Through these mechanisms, Rust advances all memory security issues to compile-time checks, making the program both efficient and safe at runtime. I hope this blog can help you understand and master the use of life cycles in Rust and write flexible and safe code in actual development! I also hope everyone supports me.