The Rust chapter states that the annotations don't tamper with the lifetime of a variable but how true is that? According to the book, the function longest takes two references of the strings and return the longer one. But here in the error case
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);
}
it does actually change the lifetime of the result variable, doesn't it?
We’ve told Rust that the lifetime of the reference returned by the longest function is the same as the smaller of the lifetimes of the references passed in.
Lifetimes are what the Rust compiler uses to keep track of how long references are valid for. Checking references is one of the borrow checker's main responsibilities. Lifetimes help the borrow checker ensure that you never have invalid references.
Rust uses lifetime parameters to avoid such potential run-time errors. Since the compiler doesn't know in advance whether the if or the else block will execute, the code above won't compile and an error message will be printed that says, “a lifetime parameter is expected in compare 's signature.”
Reference lifetime. As a reference lifetime 'static indicates that the data pointed to by the reference lives for the entire lifetime of the running program. It can still be coerced to a shorter lifetime.
The 'a reads 'the lifetime a'. Technically, every reference has some lifetime associated with it, but the compiler lets you elide (i.e. omit, see "Lifetime Elision") them in common cases. fn bar<'a>(...)
What the book is merely suggesting is that a lifetime parameter of a function cannot interfere with the affected value's lifetime. They cannot make a value live longer (or the opposite) than what is already prescribed by the program.
However, different function signatures can decide the lifetime of those references. Since references are covariant with respect to their lifetimes, you can turn a reference of a "wider" lifetime into a smaller one within that lifetime.
For example, given the definition
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str
, the lifetimes of the two input references must match. However, we can write this:
let local = "I am local string.".to_string();
longest(&local, "I am &'static str!");
The string literal, which has a 'static
lifetime, is compatible with the lifetime 'a
, in this case mainly constrained by the string local
.
Likewise, in the example above, the lifetime 'a
has to be constrained to the nested string string2
, otherwise it could not be passed by reference to the function. This also means that the output reference is restrained by this lifetime, which is why the code fails to compile when attempting to use the output of longest
outside the scope of string2
:
error[E0597]: `string2` does not live long enough
--> src/main.rs:14:44
|
14 | result = longest(string1.as_str(), string2.as_str());
| ^^^^^^^ borrowed value does not live long enough
15 | }
| - `string2` dropped here while still borrowed
16 | println!("The longest string is {}", result);
| ------ borrow later used here
See also this question for an extended explanation of lifetimes and their covariance/contravariance characteristics:
First, it's important to understand the difference between a lifetime and a scope. References have lifetimes, which are dependent on the scopes of the variables they refer to.
A variable scope is lexical:
fn main() {
let string1 = String::from("long string is long"); // <-- scope of string1 begins here
let result;
{
let string2 = String::from("xyz"); // <-- scope of string2 begins here
result = longest(string1.as_str(), string2.as_str());
// <-- scope of string2 ends here
}
println!("The longest string is {}", result);
// <-- scope of string1 ends here
}
When you create a new reference to a variable, the lifetime of the reference is tied solely to the scope of the variable. Other references have different lifetime information attached to them, depending on where the reference came from and what information is known in that context. When you put named lifetime annotations on a type, the type-checker simply ensures that the lifetime information attached to any references is compatible with the annotations.
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
// The lifetime of result cannot be longer than `'a`
result = longest(string1.as_str(), string2.as_str());
// But a reference to string2 also has lifetime `'a`, which means that
// the lifetime `'a` is only valid for the scope of string2
// <-- i.e. to here
}
// But then we try to use it here — oops!
println!("The longest string is {}", result);
}
We’ve told Rust that the lifetime of the reference returned by the longest function is the same as the smaller of the lifetimes of the references passed in.
Sort of. We did tell this information to Rust, however, the borrow-checker will still check if it is true! If it's isn't already true then we will get an error. We can't change the truthfulness of that information, we can only tell Rust the constraints we want, and it will tell us if we are right.
In your example, you could make the main
function valid by changing the lifetime annotations on longest
:
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
if x.len() > y.len() {
x
} else {
y // oops!
}
}
But now you get an error inside longest
because it no longer meets the requirements: it is now never valid to return y
because its lifetime could be shorter than 'a
. In fact, the only ways to implement this function correctly are:
x
x
&'static str
— since 'static
outlives all other lifetimesunsafe
codeIf you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With