20 Rustlings Lifetimes Solution
Lifetimes
ReadMe
Lifetimes tell the compiler how to check whether references live long enough to be valid in any given situation. For example lifetimes say "make sure parameter 'a' lives as long as parameter 'b' so that the return value is valid".
They are only necessary on borrows, i.e. references, since copied parameters or moves are owned in their scope and cannot be referenced outside. Lifetimes mean that calling code of e.g. functions can be checked to make sure their arguments are valid. Lifetimes are restrictive of their callers.
Further information
Lifetimes1.rs
// lifetimes1.rs
//
// The Rust compiler needs to know how to check whether supplied references are
// valid, so that it can let the programmer know if a reference is at risk
// of going out of scope before it is used. Remember, references are borrows
// and do not own their own data. What if their owner goes out of scope?
//
// Execute `rustlings hint lifetimes1` or use the `hint` watch subcommand for a hint.
// I AM NOT DONE
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is '{}'", result);
}
Our instructions are to help the Rust compiler with the supplied references by using lifetime annotation I presume. As always let's look at the compiler errors to see if there's any additional details.
Lifetimes1.rs Errors
⚠️ Compiling of exercises/lifetimes/lifetimes1.rs failed! Please try again. Here is the output:
error[E0106]: missing lifetime specifier
--> exercises/lifetimes/lifetimes1.rs:12:33
|
12 | 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
|
12 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++
error: aborting due to previous error
For more information about this error, try `rustc --explain E0106`.
We get a solid hint for how to help the compiler understand the lifetime of the variables, let's try to do what it suggest to do on line 12
Liftemes1.rs Solution
So as the compiler error's suggested we add a lifetime of 'a
to our function signature. First right after the function name and then we assign it to the variables as seen below.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
our full code block looks like this:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is '{}'", result);
}
So what is Actually Happening Here?
To use lifetime annotations in function signatures, we declare generic lifetime parameters inside angle brackets, just like we do with generic type parameters. The key concept is that the reference returned must stay valid as long as both parameters are valid.
In our example, we define a lifetime 'a
, and the function signature states that it takes two string slices (&'a str
) as parameters, both of which should live at least as long as 'a
. Additionally, the function signature ensures that the returned string slice also lives at least as long as 'a
. This means the reference's lifetime returned by the longest
function is constrained by the shorter of the lifetimes of the values referred to by the function arguments.
Importantly, specifying these lifetime parameters doesn't change the lifetimes of the values being passed or returned. Instead, it instructs the Rust borrow checker to reject any values that don't adhere to these constraints.
and like that...we're compiling
🎉 🎉 The code is compiling! 🎉 🎉
Output:
====================
The longest string is 'abcd'
====================
Lifetimes2.rs
// lifetimes2.rs
//
// So if the compiler is just validating the references passed
// to the annotated parameters and the return type, what do
// we need to change?
//
// Execute `rustlings hint lifetimes2` or use the `hint` watch subcommand for a hint.
// I AM NOT DONE
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
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);
}
We have a similar exercise as before in terms of using the longest
function but in this case we have an inner code block that declares string2
and also result
.
Lifetimes2.rs Errors
⚠️ Compiling of exercises/lifetimes/lifetimes2.rs failed! Please try again. Here is the output:
error[E0597]: `string2` does not live long enough
--> exercises/lifetimes/lifetimes2.rs:24:44
|
23 | let string2 = String::from("xyz");
| ------- binding `string2` declared here
24 | result = longest(string1.as_str(), string2.as_str());
| ^^^^^^^ borrowed value does not live long enough
25 | }
| - `string2` dropped here while still borrowed
26 | println!("The longest string is '{}'", result);
| ------ borrow later used here
error: aborting due to previous error
In this example, we encounter an issue where the result
variable can't be used outside the inner scope because it borrows a value (string2
) that gets dropped as soon as the scope ends. To fix this, we either move the print statement inside the scope or remove the inner scope braces, ensuring that result
remains valid.
So how do we fix it?
There's a couple of ways we could fix this but let's try the easiest (at least for me 😉).
Lifetimes2.rs Solution
So...is there any reason that we need to use the print statement outside of inner bracket? I don't think so, there's nothing preventing us from moving the last line and making this code compile, it gives us the same result and we have no issues with our variables being dropped after the scope.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
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); // add the print statment here
}
}
another easy solution is to just remove the inner scope braces and in this case we can also perform a little bit of clean-up so we don't have result
declared twice for no reason
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("hi");
let string2 = String::from("xyz");
let result = longest(string1.as_str(), string2.as_str());
println!("The longest string is '{}'", result);
}
I'm sure there's more ways we could solve this but you get the idea right? Let's move on to the next exercise.
Here's the printout of our modified code:
🎉 🎉 The code is compiling! 🎉 🎉
Output:
====================
The longest string is 'xyz'
====================
Lifetimes3.rs
// lifetimes3.rs
//
// Lifetimes are also needed when structs hold references.
//
// Execute `rustlings hint lifetimes3` or use the `hint` watch subcommand for a hint.
// I AM NOT DONE
struct Book {
author: &str,
title: &str,
}
fn main() {
let name = String::from("Jill Smith");
let title = String::from("Fish Flying");
let book = Book { author: &name, title: &title };
println!("{} by {}", book.title, book.author);
}
So we are being told that structs can also references in the instructions and we see in our errors that is telling us how to fix them.
Lifetimes3.rs Errors
⚠️ Compiling of exercises/lifetimes/lifetimes3.rs failed! Please try again. Here is the output:
error[E0106]: missing lifetime specifier
--> exercises/lifetimes/lifetimes3.rs:10:13
|
10 | author: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
9 ~ struct Book<'a> {
10 ~ author: &'a str,
|
error[E0106]: missing lifetime specifier
--> exercises/lifetimes/lifetimes3.rs:11:12
|
11 | title: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
9 ~ struct Book<'a> {
10 | author: &str,
11 ~ title: &'a str,
|
Looks easy enough let's try and implement it...
Lifetimes3.rs Solution
Lifetimes are also essential when structs hold references. In this example, we define a Book
struct with lifetime annotations for the author
and title
fields to ensure that they reference valid data. By introducing the lifetime parameter 'a
, we make sure that the references inside the struct remain valid for as long as the struct itself.
So, let's implement the lifetime notation just as the compiler suggests and let's see what happens....
struct Book<'a> {
author: &'a str,
title: &'a str,
}
fn main() {
let name = String::from("Jill Smith");
let title = String::from("Fish Flying");
let book = Book { author: &name, title: &title };
println!("{} by {}", book.title, book.author);
}
Easy enough we are compiling!
🎉 🎉 The code is compiling! 🎉 🎉
Output:
====================
Fish Flying by Jill Smith
====================
Conclusion
In conclusion, lifetimes are a critical part of Rust's ownership system that helps ensure the safety and validity of references. By understanding how to use lifetime annotations in function signatures and structs, you can write code that is both safe and efficient. Remember that lifetimes don't change the actual lifetimes of values but serve as constraints for the borrow checker to enforce.