The original link: https://hashrust.com/blog/lifetimes-in-rust/ the original title: Lifetimes in Rust public number: Rust

Introduction to the

For many Rust beginners, lifetime is a difficult concept to master. I struggled with Rust for a while before I realized how important the life cycle was for the compiler to do its job. The life cycle is not inherently difficult. It’s just that it’s such a new idea that most programmers have never seen them in any other language. To make matters worse, people overuse the word lifetime to talk about many closely related ideas. In this article, I’ll make a distinction between these ideas so that you can think clearly about the life cycle.

Purpose of LifeTimes

Before we get into specifics, let’s understand why life cycles exist. What are they used for? The life cycle helps the compiler enforce a simple rule: No reference should outlive its referent. In other words, the lifecycle helps the compiler suppress dangling pointer bugs. As you will see in the following examples, the compiler accomplishes this by analyzing the lifecycle of the associated variables. If the lifetime of a reference is less than that of the object to which it refers, the code can compile; otherwise, it cannot compile.

The Meaning of the word lifetime

Part of the reason life cycles are so confusing is that in much of Rust’s writing, the word life cycle is used loosely to refer to three different things — real life cycles of variables, life cycle constraints, and life cycle notations. Let’s take a look one by one.

Lifetimes of Variables

This is intuitive. The life cycle of a variable is how long it’s alive. The usefulness of this meaning is closest to the dictionary definition, the duration of a thing’s existence or usefulness. For example, in the following code, the life cycle of X continues to the end of the external block, while the life cycle of y ends at the end of the internal block.

{

    let x: Vec<i32> = Vec::new();//---------------------+

    {// |

        let y = String::from("Why");//---+ | x's lifetime

        // | y's lifetime |

    }// <--------------------------------+ |

}// <---------------------------------------------------+

Copy the code

Lifetime constraints

The way variables interact in code has certain constraints on their life cycle. For example, in the following code, x=&y; This row adds a constraint that the life cycle of X should be enclosed within the life cycle of y.

//error:`y` does not live long enough

{

    let x: &Vec<i32>;

    {

        let y = Vec::new();//----+

// | y's lifetime

// |

        x = &y;//----------------|--------------+

// | |

    }// <------------------------+ | x's lifetime

    println!("x's length is {}", x.len());// |

}// <-------------------------------------------+

Copy the code

If I didn’t add this constraint, x would end up in println! This line of code accesses invalid memory because x is a reference to y, and y would have been destroyed on the previous line. Remember that constraints do not change the real life cycle — the life cycle of X, for example, still extends to the end of the external block; they are just tools used by the compiler to disallow dangling references. And in the example above, the real life cycle does not satisfy the constraint: the life cycle of X exceeds the life cycle of Y. Therefore, this code will fail to compile.

Lifetime Annotations

As you saw in the last section, many times the compiler generates all the lifecycle constraints. But as the code gets more complex, the compiler lets the programmer add constraints manually. Programmers do this through lifecycle notation. For example, in the following code snippet, the compiler needs to know whether the reference returned by the print_net function is borrowed from S1 or S2. So the compiler lets the programmer explicitly add this constraint:

//error:missing lifetime specifier

//this function's return type contains a borrowed value,

//but the signature does not say whether it is borrowed from `s1` or `s2`

fn print_ret(s1: &str, s2: &str) - > &str {

    println!("s1 is {}", s1);

    s2

}

fn main() {

    let some_str: String = "Some string".to_string();

    let other_str: String = "Other string".to_string();

    let s1 = print_ret(&some_str, &other_str);

}

Copy the code

If you want to know why the compiler cannot see that the output references are borrowed from S2, read the answer [1] on StackOverflow. To find out when the compiler asks you to omit annotations, see the lifecycle omission section below.

The programmer then annotates s2 and the returned reference with ‘a ‘to tell the compiler that the return value is borrowed from S2:

fn print_ret<'a>(s1: &str, s2: &'a str) - > &'a str {

    println!("s1 is {}", s1);

    s2

}

fn main() {

    let some_str: String = "Some string".to_string();

    let other_str: String = "Other string".to_string();

    let s1 = print_ret(&some_str, &other_str);

}

Copy the code

I would like to emphasize that the lifecycle annotation ‘a appears on both the argument s2 and the returned reference. Do not interpret this to mean that s2 and the returned reference have exactly the same lifecycle, but rather that the returned reference using the ‘a annotation borrows from the argument with the same annotation. Because S2 borrows from other_str, the lifecycle constraint here is that the returned reference must not live longer than other_str. Code compiles because lifecycle constraints are met:

fn print_ret<'a>(s1: &str, s2: &'a str) - > &'a str {

    println!("s1 is {}", s1);

    s2

}

fn main() {

    let some_str: String = "Some string".to_string();

    let other_str: String = "Other string".to_string();//-------------+

    let ret = print_ret(&some_str, &other_str);//---+ | other_str's lifetime

    // | ret's lifetime |

}// <-----------------------------------------------+-----------------+



Copy the code

Before I show you more examples, let me briefly introduce the lifecycle annotation syntax. To create a lifecycle annotation, you must first declare a lifecycle parameter. For example, <‘a> is a lifecycle declaration. The lifecycle parameter is a generic parameter. You can read <‘a> as “for a lifecycle ‘a…” . Once a lifecycle parameter is declared, it can be used in a reference to create a lifecycle constraint. Remember, by tagging references with ‘a, the programmer is simply stating certain constraints; It is the job of the compiler to find a specific lifecycle for ‘a ‘that satisfies the constraint.

More examples

Next, consider a function min that finds the smallest of two values:

fn min<'a>(x: &'a i32, y: &'a i32) - > &'a i32 {

    if x < y {

        x

    } else {

        y

    }

}

fn main() {

    let p = 42;

    {

        let q = 10;

        let r = min(&p, &q);

        println!("Min is {}", r);

    }

}

Copy the code

Here, the ‘a lifecycle parameter is marked with the parameters x, y, and the return value. It means that the return value can be borrowed from either x or y. Because x and y borrow from p and q, respectively, the life cycle of the returned reference should be enclosed within both P and q. This code compiles because the constraint is satisfied:

fn min<'a>(x: &'a i32, y: &'a i32) - > &'a i32 {

    if x < y {

        x

    } else {

        y

    }

}

fn main() {

    let p = 42;//-------------------------------------------------+

    {// |

        let q = 10;//------------------------------+ | p's lifetime

        let r = min(&p, &q);//------+ | q's lifetime |

        println!("Min is {}", r);// | r's lifetime | |

    }// <---------------------------+--------------+ |

}// <-------------------------------------------------------------+

Copy the code

In general, when two or more arguments in a function are labeled with the same lifecycle argument, the returned reference must not live longer than the smallest parameter lifecycle. One last example. A common mistake many new C++ programmers make is to return a pointer to a local variable. Similar attempts are not allowed in Rust:

//Error:cannot return reference to local variable `i`

fn get_int_ref<'a> () - > &'a i32 {

    let i: i32 = 42;

    &i

}

fn main() {

    let j = get_int_ref();

}

Copy the code

Because there are no arguments in get_int_ref, the compiler knows that the returned reference must be borrowed from a local variable, which is not allowed. The compiler correctly dodged the disaster because the local variable was already cleared when the returned reference tried to access it:

fn get_int_ref<'a> () - > &'a i32 {

    let i: i32 = 42;//-------+

    &i// | i's lifetime

}// <------------------------+

fn main() {

    let j = get_int_ref();//-----+

// | j's lifetime

}// <----------------------------+

Copy the code

Lifetime omission

When the compiler asks the programmer to omit a lifecycle annotation, it is called lifetime elision. Again, lifecycle omission is misunderstood – how can lifecycle be omitted when it is inextricably linked to the creation and destruction of variables? What is omitted is not the lifecycle, but the lifecycle annotation and the corresponding lifecycle constraints. In earlier versions of the Rust compiler, lifecycle annotations were not allowed to be omitted and each lifecycle annotation was required. But over time, the compiler team observed that the same lifecycle annotations were repeated, so the compiler was modified to begin to derive lifecycle annotations. Programmers can omit lifecycle annotations in the following cases:

  1. When there is only one input reference. In this case, the input lifecycle annotation is given references to all the outputs. For example, fn some_func(s: & STR) -> &str would be derived as fn some_func<‘a>(s: &’a STR) -> &’a STR.

  2. When there are multiple input references, but the first argument is either &self or &mut self. In this case, the input lifecycle annotation is also given all output references. For example, fn some_method(&self) -> &str is equivalent to fn some_method<‘a>(&’a self) -> &’a STR.

Lifecycle omitting reduces clutter in the code, and it is possible that in the future, the compiler will be able to deduce lifecycle constraints for more patterns.

Conclusion (Conclusion)

Many newcomers to Rust find the subject of life cycles difficult to understand. But it is not the lifecycle itself that is to blame, but how the concept is presented in much of Rust’s writing. In this article, I have tried to explain what the overused term life cycle means. The life cycle of a variable must meet specific constraints imposed by the compiler and programmer before the compiler can make sure the code is sound. Without the lifecycle facility, the compiler will not be able to guarantee the security of most Rust programs.

This article is prohibited from reprint, thank you for your cooperation! Welcome to my wechat official account: Rust

Rust

The resources

[1]

This StackOverflow answer: https://stackoverflow.com/a/31612025