Lifetimes
The concept of lifetimes is related to how long variables are valid. Even though lifetimes are a rather complex concept, there are two common cases one has to consider:
- Variables are dropped at the end of their defined scope.
- References might need an explicit lifetime notation.
Scopes
By default, variables are dropped at the end of their defined scope.
fn main() { let x: usize = 0; } // x is dropped here.
The example above gets more interesting if we add a nested scope inside main. Try running the code below and see what happens.
fn main() { { let x: usize = 0; } // x is dropped here. println!("{x}"); }
The variable x goes out of scope before we try to print it, which is why the compiler complains. Switching the order around by defining x in the outer scope and printing it in the inner scope works, because x is still valid there.
fn main() { let x: usize = 0; { // x is still valid here. println!("{x}"); } } // x is dropped here.
The same principle applies to function scopes. Unless we return, variables defined inside a function scope will be dropped at the end.
fn my_function() -> String { let x: String = "my_string".to_string(); // Anything else we define here and don't return // will be dropped at the end of the function scope. return x; } fn main() { let x = my_function(); println!("{x}"); }
Lifetime notation
To illustrate explicit lifetime notation, we'll create a Struct with a single field my_string, which is of type &str.
struct MyStruct { my_string: &str } fn main() { let my_struct = MyStruct {my_string: "Hello, world!"}; }
Try running the code and see what happens. We get a compiler error, stating that we need a lifetime parameter. Why is this? my_string is of type &str, which means that MyStruct does not own it. This also means MyStruct does not control when my_string is no longer valid. This is dangerous, because if my_string would get dropped and we subsequently try to read its value in MyStruct, we'd be in trouble. The Rust compiler needs some kind of assurance that MyStruct and my_string will both be valid for at least as long as each other. This is what lifetimes are for.
Lifetimes are signified with a ', followed by a name. E.g., 'a would be a lifetime called a. To make the code run, we'll bind MyStruct and my_string to the same lifetime, telling the Rust compiler that MyStruct will live for at least as long as my_string.
struct MyStruct<'a> { my_string: &'a str } fn main() { let my_struct = MyStruct {my_string: "Hello, world!"}; }
The same concept applies to functions. In the following example, we'll define a function that takes no arguments and returns a &str.
fn my_function<'a>() -> &'a str{ let x: &'a str = "my_string"; return x; } fn main() { let x = my_function(); println!("{x}"); }