mirror of
https://github.com/darkicewolf50/RustBrock.git
synced 2025-06-15 04:54:17 -06:00
462 lines
19 KiB
Markdown
462 lines
19 KiB
Markdown
# Lifetimes
|
|
|
|
## Valid References with Lifetimes
|
|
|
|
Lifetimes are another type of generic generic that has been used in the past.
|
|
|
|
Rather than ensuring that a type has the behavior we want lifetimes ensure that references are valid for as long as we need them to be.
|
|
|
|
Every reference in Rust has a *lifetime*, which is the scope that a reference is valid
|
|
|
|
A majority of the time references are implicit and inferred, like how types are inferred for the most part
|
|
|
|
We must only annotate types when multiple types are possible
|
|
|
|
The same concept can happen with lifetimes, where references could be related in a few different ways
|
|
|
|
Rust requires us to annotate the relationships using generic lifetime parameters to ensure the actual references used at runtime are valid
|
|
|
|
Annotating lifetimes is not a concept or feature that is in other programming languages
|
|
|
|
## Preventing Dangling References with Lifetimes
|
|
|
|
The main aim of lifetimes is to prevent *dangling references*, this causes a program to reference data other than the data it's intended to reference
|
|
|
|
Here is an example that will not compile due to the value being referred to going out of scope, then the reference being invalid and being used elsewhere
|
|
|
|
```rust
|
|
fn main() {
|
|
let r;
|
|
|
|
{
|
|
let x = 5;
|
|
r = &x;
|
|
}
|
|
|
|
println!("r: {r}");
|
|
}
|
|
```
|
|
|
|
This is initialization is allowed because if yo try to use a variable with no value associated then Rust will throw a compile-time error.
|
|
|
|
This proves that rust does not allow or have a null type
|
|
|
|
in this code `r` has a reference to `x` but `x` goes out of scope, this makes `r` not be valid reference, this invalid reference is attempted to be used.
|
|
|
|
The error message that would occur afterwards would say that `x` "does not live long enough". This refers to `x` going out of scope but `r` is still in scope
|
|
|
|
Hence we say that `r` "lives longer" because it has a larger scope
|
|
|
|
## The Borrow Checker
|
|
The Rust compiler has a *borrow checker* that compares scopes to determine whether all borrows are valid
|
|
|
|
Here is an example with annotations showing the lifetimes of variables
|
|
```rust
|
|
fn main() {
|
|
let r; // ---------+-- 'a
|
|
// |
|
|
{ // |
|
|
let x = 5; // -+-- 'b |
|
|
r = &x; // | |
|
|
} // -+ |
|
|
// |
|
|
println!("r: {r}"); // |
|
|
} // ---------+
|
|
```
|
|
|
|
In this annotations `r` is represented by `'a` and `x` is represented by `'b`
|
|
|
|
`'a` is the lifetime of `r` and `'b` is the lifetime of `x`
|
|
|
|
As you can see `'a` encompasses more than `'b`
|
|
|
|
At compile time Rust will compare the size of the two lifetimes and sees that `r` has a lifetime of `'a` but that it refers to memory with a lifetime of `'b`. This results in a rejection of the program because the reference becomes invalid before a use of it: the subject reference doesn't live as long as the reference.
|
|
|
|
The lifetime refers to how long it "lives" in memory or is available in memory
|
|
|
|
Here is the fix to the program above
|
|
```rust
|
|
fn main() {
|
|
let x = 5; // ----------+-- 'b
|
|
// |
|
|
let r = &x; // --+-- 'a |
|
|
// | |
|
|
println!("r: {r}"); // | |
|
|
// --+ |
|
|
} // ----------+
|
|
```
|
|
|
|
As you can now see the the lifetime of `'b` is now larger than the `'a` reference
|
|
|
|
Know you know that the reference `r` will always be valid because `x` is always valid
|
|
|
|
|
|
## Generic Lifetimes in Functions
|
|
|
|
To illustrate this we will write a function that returns the longer of two string slices
|
|
|
|
This function takes in two string slices and returns a single string slice
|
|
|
|
Here is an example of the use of the function `longest` that does this functionality
|
|
```rust
|
|
fn main() {
|
|
let string1 = String::from("abcd");
|
|
let string2 = "xyz";
|
|
|
|
let result = longest(string1.as_str(), string2);
|
|
println!("The longest string is {result}");
|
|
}
|
|
```
|
|
|
|
Note that `longest` takes in a string slice, which are references, we don't want the function to take ownership of the strings
|
|
|
|
If we try to implement `longest` function like below, it won't compile
|
|
```rust
|
|
fn longest(x: &str, y: &str) -> &str {
|
|
if x.len() > y.len() {
|
|
x
|
|
} else {
|
|
y
|
|
}
|
|
}
|
|
```
|
|
|
|
Here is the compiler error
|
|
```
|
|
$ cargo run
|
|
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
|
|
error[E0106]: missing lifetime specifier
|
|
--> src/main.rs:9:33
|
|
|
|
|
9 | fn longest(x: &str, y: &str) -> &str {
|
|
| ---- ---- ^ expected named lifetime parameter
|
|
|
|
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
|
|
help: consider introducing a named lifetime parameter
|
|
|
|
|
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
|
|
| ++++ ++ ++ ++
|
|
|
|
For more information about this error, try `rustc --explain E0106`.
|
|
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
|
|
```
|
|
|
|
This error highlights that the return type needs a generic lifetime parameter because rust cannot determine whether the reference is referring to `x` or `y`
|
|
|
|
We don't know either because of the `if` and `else` in the body of the function refer to two different things depending on what the inputs are
|
|
|
|
We also don't know the concrete lifetimes of the references , so we cannot even analyze the lifetimes like we did before, so we cant ensure that the reference and lifetime will always be valid.
|
|
|
|
The borrow checker cannot check for this either so it throws an error.
|
|
|
|
This is because it doesn't know the lifetimes of `x` and `y` relate to the lifetime of the return value
|
|
|
|
To fix this error we should add a generic lifetime parameter that defines the relationship between the references so the borrow checker can perform its analysis
|
|
|
|
## Lifetime Annotation Syntax
|
|
Lifetime annotations don't change based on how long any of the references live, instead they describe the relationship of the lifetimes of multiple references to each other without affecting the lifetimes
|
|
|
|
Like how functions can accept any type when the signature specifies a generic, functions can accept any lifetime by specifying a generic lifetime parameter
|
|
|
|
Lifetime annotations have a unusual syntax: the naming of a lifetime parameter must start with an apostrophe `'` and are usually all lowercase and very short, just like generics
|
|
|
|
Most people use `'a` as the first lifetime annotation
|
|
|
|
The placement of the lifetime goes after the `&` reference using a space to separate the lifetime from the reference's type
|
|
|
|
Here is an example to an `i32` reference
|
|
```rust
|
|
&i32 // a reference
|
|
&'a i32 // a reference with an explicit lifetime
|
|
&'a mut i32 // a mutable reference with an explicit lifetime
|
|
```
|
|
One annotation by itself has no meaning because cannot be a relationship to another (or multiple) reference(s)
|
|
|
|
## Lifetime Annotations in Function Signatures
|
|
To use lifetime annotations in a function's signature you must first declare the generic lifetime parameter inside `<>` between the function name and the parameter list (just like generics)
|
|
|
|
In this case the constraint we have is that we want the returned reference to be valid as long as both parameters are valid
|
|
|
|
This will be the relationship between the lifetime of the parameters and the return reference.
|
|
|
|
Here is this implemented with the name `'a` as the lifetime, it is added to each reference in the function signature
|
|
```rust
|
|
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
|
|
if x.len() > y.len() {
|
|
x
|
|
} else {
|
|
y
|
|
}
|
|
}
|
|
```
|
|
This code should compile now with the use of lifetime annotations
|
|
|
|
The function signature now tells Rust that for some lifetime `'a`, the function takes two parameters, both of which are string slices that live at least as long as lifetime `'a`
|
|
|
|
The signature also tells Rust that the string slice returned from the function will live at least as long as life `'a`.
|
|
|
|
What this means is that the smaller of the lifetime returned by the `longest` function is the same as the smaller of the lifetime values referred to by the function arguments
|
|
|
|
These relationships are what we want Rust to use when analyzing this code
|
|
|
|
Lifetime annotations go in the signature NOT the body of the function
|
|
|
|
Lifetime annotations are part of the contract of the function, like types in the signature
|
|
|
|
This makes the lifetime analysis easier on the Rust compiler
|
|
|
|
If there is a problem in the signature it makes it easier to identify and express in an error as well, and give clear solutions
|
|
|
|
|
|
Here is another example of the use of the `longest` function
|
|
```rust
|
|
fn main() {
|
|
let string1 = String::from("long string is long");
|
|
|
|
{
|
|
let string2 = String::from("xyz");
|
|
let result = longest(string1.as_str(), string2.as_str());
|
|
println!("The longest string is {result}");
|
|
}
|
|
}
|
|
```
|
|
In this example the lifetime of the return value of the `longest` function is the same as `string2` which means that after the inner scope or `{}` the reference is no longer valid
|
|
|
|
This is both due to `string2`, `result` (which stores the reference) and the function signature states that it only lives as long as the shortest function
|
|
|
|
The value reference in `result` is `long string is long` and the program will print `The longest string is long string is long`
|
|
|
|
In this example the program will not compile
|
|
```rust
|
|
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}");
|
|
}
|
|
```
|
|
This is due to `string2` not living long enough for the reference in `result` to be used while in a valid state
|
|
|
|
Here is the error it would provide
|
|
```
|
|
$ cargo run
|
|
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
|
|
error[E0597]: `string2` does not live long enough
|
|
--> src/main.rs:6:44
|
|
|
|
|
5 | let string2 = String::from("xyz");
|
|
| ------- binding `string2` declared here
|
|
6 | result = longest(string1.as_str(), string2.as_str());
|
|
| ^^^^^^^ borrowed value does not live long enough
|
|
7 | }
|
|
| - `string2` dropped here while still borrowed
|
|
8 | println!("The longest string is {result}");
|
|
| -------- borrow later used here
|
|
|
|
For more information about this error, try `rustc --explain E0597`.
|
|
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
|
|
```
|
|
|
|
This also states that `string2` would need to be valid until the `print!` macro. Hence why it states that it doesn't live long enough
|
|
|
|
Even though the reference in this case is to `string1` the compiler and the function signature states that the lifetime of the return value is the same as the shortest lifetime
|
|
|
|
## Thinking in Terms of Lifetimes
|
|
The way that you need to specify lifetime parameters depends on what your function is doing
|
|
|
|
For example if this was your function
|
|
```rust
|
|
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
|
|
x
|
|
}
|
|
```
|
|
This function's return reference's lifetime is the same as `x` so the returned reference only lives as long as what is passed into `x`
|
|
|
|
The return reference lifetime has no relations to the `y` lifetime
|
|
|
|
Any lifetime MUST have another relationship to another reference
|
|
|
|
Any lifetime in a function signature's return value MUST relate to AT LEAST one parameter
|
|
|
|
If it doesn't it would create a dangling reference, this is because the value would go out of scope at the end of the function
|
|
|
|
Here is an example of `longest` that creates both a dangling reference and an invalid lifetime
|
|
```rust
|
|
fn longest<'a>(x: &str, y: &str) -> &'a str {
|
|
let result = String::from("really long string");
|
|
result.as_str()
|
|
}
|
|
```
|
|
|
|
Here is the error
|
|
```
|
|
$ cargo run
|
|
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
|
|
error[E0515]: cannot return value referencing local variable `result`
|
|
--> src/main.rs:11:5
|
|
|
|
|
11 | result.as_str()
|
|
| ------^^^^^^^^^
|
|
| |
|
|
| returns a value referencing data owned by the current function
|
|
| `result` is borrowed here
|
|
|
|
For more information about this error, try `rustc --explain E0515`.
|
|
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
|
|
```
|
|
The problem is that `result` goes out of scope and gets cleaned up at the end of the function.
|
|
|
|
We also try to return a reference at the end of the function to `result`
|
|
|
|
There is no way we can specify lifetime parameters that would change the dangling reference in this case
|
|
|
|
Rust will not let you create a dangling reference
|
|
|
|
The fix for this would be to transfer ownership out of the function
|
|
|
|
## Lifetime Annotations in Struct Definitions
|
|
Structs can also hold references
|
|
|
|
When it holds a reference it needs a lifetime annotation on every reference in the struct's def
|
|
|
|
Here is an example where the struct holds a single string
|
|
```rust
|
|
struct ImportantExcerpt<'a> {
|
|
part: &'a str,
|
|
}
|
|
|
|
fn main() {
|
|
let novel = String::from("Call me Ishmael. Some years ago...");
|
|
let first_sentence = novel.split('.').next().unwrap();
|
|
let i = ImportantExcerpt {
|
|
part: first_sentence,
|
|
};
|
|
}
|
|
```
|
|
|
|
Notice that the lifetime annotation is the same as a function's signature
|
|
|
|
The lifetime goes in a `<>` after the name of the structure
|
|
|
|
This annotation means that an instance of the struct can't outlive the reference
|
|
|
|
## Lifetime Elision
|
|
In this case the function does not have a lifetime annotation and it compiles
|
|
```rust
|
|
fn first_word(s: &str) -> &str {
|
|
let bytes = s.as_bytes();
|
|
|
|
for (i, &item) in bytes.iter().enumerate() {
|
|
if item == b' ' {
|
|
return &s[0..i];
|
|
}
|
|
}
|
|
|
|
&s[..]
|
|
}
|
|
```
|
|
|
|
The reason why this function compiles without lifetime annotations is due to previous versions of Rust
|
|
|
|
In earlier versions of Rust (pre-1.0), this code wouldn't have compiled because every reference needed an explicit lifetime
|
|
|
|
At that time a function signature would have been written like this
|
|
```rust
|
|
fn first_word<'a>(s: &'a str) -> &'a str {
|
|
```
|
|
|
|
After writing a lot of Rust code, the Rust development team found that Rust programmers were writing a lot of the same code
|
|
|
|
These patterns were predictable and followed a few deterministic patterns. So the development team allowed the compiler and borrow checker to infer the lifetime annotation by using these common lifetime annotation patterns
|
|
|
|
Its important to know that it is possible that more deterministic patterns will emerge and be added to the compiler.
|
|
|
|
In the future even fewer lifetime annotations might be required
|
|
|
|
The patterns programmed into Rust's lifetime analysis of references are called the *lifetime elision rules*
|
|
|
|
These aren't rules for programmers, they are a set of particular cases that the compiler will consider. If your code fits these cases you don't have two explicitly define the lifetimes.
|
|
|
|
The elision rules don't provide full inference.
|
|
|
|
If there is still ambiguity as to what lifetimes are after the compiler applies the rules, the compiler will give an error instead of guessing
|
|
|
|
Instead of guessing the compiler will give you an error that you can resolve by adding the lifetime annotations
|
|
|
|
Lifetimes on a function or method parameters are called input lifetimes, and lifetimes on return values are called *output lifetimes*
|
|
|
|
The compiler uses three rules to figure out the lifetimes of the references when there aren't explicit annotations
|
|
|
|
The first rule applies to input lifetimes and the second and third rules apply to output lifetimes
|
|
|
|
If the complier gets to the end of these three rules and still cant figure it out then the compiler will stop with an error
|
|
|
|
These rules apply to `fn` definitions and `impl` blocks
|
|
|
|
The first rule is that the compiler assigns a lifetime parameter to each parameter that is a reference.
|
|
|
|
Each parameter gets its own separate lifetime annotation that has no relationship to any other lifetime (`fn foo<'a, 'b>(x: &'a i32, y: &'b i32`)
|
|
|
|
The second rule is that if there is exactly one input parameter, that lifetime is assigned to all output parameters (`fn foo<'a>(x: &'a i32) -> &'a i32`)
|
|
|
|
The third rule is that there are multiple input lifetime parameters, but one of them is `&self` or `&mut self`. Due to it being a method, the lifetime of `self` is assigned to all output lifetime parameters
|
|
|
|
This rule makes methods much cleaner to read and write because fewer symbols are necessary
|
|
|
|
## Lifetime Annotations in Method Definitions
|
|
When we implement methods on struct with lifetimes, we use the same syntax as generic type parameters.
|
|
|
|
Where we declare and use the lifetime parameters depends on whether they are related to the struct fields or the method parameters and return values
|
|
|
|
In method signatures inside the `impl` block, references might be tied to the lifetime of references in the strut's fields or they might be independent
|
|
|
|
The lifetime elision rules often make it so that lifetime annotations aren't necessary in method signatures
|
|
|
|
Lets look at some examples using the struct `ImportantExcerpt`
|
|
|
|
First the method named `level` whose only parameter is a reference to `self` and whose return value is an `i32`, which is not a reference to anything
|
|
```rust
|
|
impl<'a> ImportantExcerpt<'a> {
|
|
fn level(&self) -> i32 {
|
|
3
|
|
}
|
|
}
|
|
```
|
|
The lifetime parameter declaration after `impl` and its use after the type name are required but we're not required to annotate the lifetime of the reference to `self` because of the first elision rule
|
|
|
|
Here is an example where the third elision rule is applicable
|
|
```rust
|
|
impl<'a> ImportantExcerpt<'a> {
|
|
fn announce_and_return_part(&self, announcement: &str) -> &str {
|
|
println!("Attention please: {announcement}");
|
|
self.part
|
|
}
|
|
}
|
|
```
|
|
|
|
There are two input lifetimes, so Rust applies the first lifetime elision rule and give both `&self` and `announcement` their own lifetimes.
|
|
|
|
Then because, because one of the parameters is `&self` the return type gets the lifetime of `&self` and all lifetimes have been dealt with
|
|
|
|
## The Static Lifetime
|
|
One special lifetime is the `'static`, which denotes that the affected reference *can* live for the entire duration of the program
|
|
|
|
All string literals have the `'static` lifetime always
|
|
|
|
Here is how we can annotate it
|
|
```rust
|
|
let s: &'static str = "I have a static lifetime.";
|
|
```
|
|
|
|
The string literal is stored in the program's binary which is always available
|
|
|
|
You might see suggestions to use `'static` lifetime in the error message.
|
|
|
|
Think about whether or not the value being referenced/the reference will always be valid before adding it
|
|
|
|
Most of the time the suggestion comes from attempting to create a dangling reference or a mismatch of the available lifetimes, instead of adding a `'static` lifetime annotation
|
|
|
|
Instead the solution is to fix those problems |