SoFunction
Updated on 2025-04-09

Verification of the validity of references and preventing dangling references in Rust life cycle

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 variablesrSet to reference inner variablesxbut the inner variable is cleaned after the scope ends, thus making the referencerPoint 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 becausexLife cycle ratiorShort, no guaranteerThe 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'aand'bMarked separatelyrandxLife 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'aand'b,DiscoverrThe life cycle (outer layer) is greater than what it refers toxThe 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,xThe statement moved torWithin 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 confirmrThe 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 toxstillyThe 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'axandyThe quote is at least active'atime, and the returned reference is guaranteed to be at least active'a. When we calllongestWhen the compiler selectsxandyThe shorter life cycle in the period is used as the actual life cycle of the reference.

For example, in the following code,string1The life cycle is longer thanstring2life cycle, so the life cycle of the returned reference is shorterstring2scope 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'aand use it in fieldsparton the reference type. This meansImportantExcerptAn 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

'staticis a special life cycle that means that references can be valid throughout the entire program execution. All string literals have'staticlife 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 versionlongestFunction, which not only returns a longer string slice, but also accepts an extra parameterann, this parameter is required to be implementedDisplayFeatures (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.'aand generic type parametersTTpasswhereThe 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 examplelongestCorrect 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'staticLifecycle 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.