Just so everyone knows, the text is from this chapter in the Book. I don't really like the Book's presentation, since it conflates the liveness scopes of values with Rust lifetimes (those '_
things), and conflates lifetimes with scopes, without pointing out that these are coarse approximations to give you the general idea. (This is the section where the quote comes from. I don't actually know what the quote from the book was trying to convey either.)
So one thing I want to highlight for you is that Rust lifetimes (those '_
things) are not the same as the liveness scopes of values. The Book (and to be fair, most other documentation or other discussions about Rust) doesn't take care to distinguish the two, but I will.
The main connection between the two is that when the scope of a value ends -- e.g. when a value goes out of a scope -- is a use of the value. The borrow checker looks for uses of values that conflict with outstanding borrows. The compiler uses lifetimes ('_
) as part of the analysis of which borrows are outstanding.
The compiler does not use lifetimes to decide the liveness scopes of values. This may be what the quote was trying to convey.
Lifetimes ('_
) don't exist at run-time at all.
I'm not sure exactly what you mean by "life cycle [of the variable]".
If you change the type of the return value, for example by changing a lifetime ('_
), you do change the meaning of the function signature. The function signature defines an API contract that callers must fulfill to call the function, and that the function body must also fulfill in order to compile. You can't call getLongLifeRef(&arg)
because you'll get a borrow check error at the call site if you try to create &'static str
from &arg
. And if you made this change:
// vv vv
fn getLongLifeRef<'a>(arg:&'a str)->&'static str{
return arg
}
The function body would get an error because 'a
isn't known to be 'static
.
More generally, annotation can definitely change the lifetime ('_
) of a reference in some sense. Namely, you can force the type of a reference to have a specific lifetime ('_
).
fn example<'a>() {
// This fails to compile; removing either annotation compiles
let x: &'a str = "some str";
let y: &'static str = x;
}
(Usually you don't need or want such annotations inside function bodies.)
But if a program compiles before and after you change some lifetime annotations (but nothing else), the liveness scopes of values -- for example, where a String
gets dropped -- does not change.
Maybe this last point is what the quote meant.
Just after the quote, they mention functions, even though they're not in the "function signature" section yet. So let's consider particularly the context of functions which are generic over lifetimes.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
Lifetimes introduced in the signature of a function have an implicit property: they are always at least just longer than the function body, so that they are valid everywhere within the function. This is why you can never borrow a local variable for a lifetime introduced in the signature: every local variable is moved or goes out of scope by the end of the function, and that's incompatible with being borrowed.
The generic lifetimes are also chosen by the caller of the function. Because the caller chooses the lifetimes, the only other things the body of the function can assume about the lifetime come from the annotation and lifetime bounds in the signature. In the signature above, the annotations mean that the lifetimes of the two input types, and that of the output lifetime, are all the same. If you gave the input lifetimes different names, the function body could no longer assume they were the same.
But no matter what the bounds and annotations are, if the lifetimes are introduced by the function, they still must be longer than the function body, and they are still chosen by the caller. The exact annotations don't change that.
Maybe that's what the quote meant.
Anyway, being only able to assume things about the lifetimes which are stated in the function signature is similar to how type generics work:
// If you remove `T: Clone`, this won't compile, because the caller
// chooses `T` and you can't assume it implements `Clone` without
// making the requirement explicit.
fn my_clone<T: Clone>(t: &T) -> T {
t.clone()
}
// This doesn't work because the types don't match.
// The caller chooses both types and you can't assume they're the same.
// fn my_push<T, U>(vec: &mut Vec<T>, item: U) {
// vec.push(item);
// }
// Here we've made them the same by giving them the same "type name" `T`.
fn my_push<T>(vec: &mut Vec<T>, item: T) {
vec.push(item);
}
The longest
function requires the input lifetimes to be the same, similar to how my_push
requires the type T
to be the same in vec: Vec<T>
and item: T
.