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}"); }