I wonder why every time this question is asked the answers are about how lifetime work, not about why they do exist.
The answer is simple: lifetimes are not, really, part of your program. Not in Rust. Maybe in some other language they would be part of the program but Rust they are not, Existential proof: mrustc doesn't support them, gccrs doesn't support them yet (but they promise to support before release), etc.
Ok. If lifetimes are not part of your programs then why the heck they even exist at all? The answer is not that hard, actually: lifetimes are embedded proof of correctness of your program which reader and, more importantly, compiler, may understand.
This makes Rust a language half-way between something like “normal” programming language and something like Google Wuffs and/or formally verified C.
The differences are two-fold:
- Lifetimes only guaranteed limited amount of correctness (they wouldn't catch simple type where you write
a + b
instead of a - b
).
- Lifetimes are compact (in many languages with formal proofs said proofs may be not just larger than actual code, but much larger than actual code) and that means Rust may declare them mandatory, non-optional.
Experience have shown that error messages which compiler may provide if you specify your lifetime annotations right are extremely valuable and people like them. But not everyone and newbies, invariably, pass through the stage of “why do these sigils icons exist and why do they prevent me from creating program freely”?
The answer is: they are not for “you”, they are for “future you”. Sometimes for short-term future you (if compiler notices that your code misbehaves every time you call certain function and you avoid crashes), most of the time for long-term future you (if you keep reference to HashSet
element while some other code adds items then for a long time nothing would work, but sooner or latter HashSet
would resize itself and you spend days debugging fragile and hard to reproduce heisenbugs).
And if you understand what lifetimes are and why lifetimes exist… you can see why you may want to have different lifetime for different references in the same struct.
Now you can see why sometimes you need different lifetimes in a struct and why sometimes you need the same lifetimes.
Consider the following code:
struct Pair<'a> {
a: &'a str,
b: &'a str,
}
fn change<'a>(pair: Pair<'a>) -> Pair<'a> {
pair
}
Just a pair of references and function that does nothing. But because we used one lifetime sigil it says, basically: “I have no idea how long refences are valid, but you said that I can assume they exist for the sime time”. Now compiler may look on your code and apply that knowledge. Consider this function:
fn foo(x: &str) -> &str {
let y = String::from("Something");
let pair = Pair { a: x, b: &y };
let changed_pair = change(pair);
changed_pair.a
}
Compiler doesn't know what you wanted to achieve, but it does know that your proof of correctness… it's incorrect. You said that references a
and b
exist for the same time, but your y
object lives and dies in your function yet you try to return a
from that function!
How can we fix that? By adding another lifetime and making sure a
and b
live for the different time and compiler knows that.
Does it mean that it's always better to have two lifetimes? No. For structs it's almost always the best choice, but for functions it's not.
Consider the following modification of change
function:
fn change<'a>(pair: Pair<'a, 'a>) -> Pair<'a, 'a> {
let mut new_pair = pair;
core::mem::swap(&mut new_pair.a, &mut new_pair.b);
new_pair
}
This function is correct precisely because we promised to the compiler that references a
and b
live for the same time… but, of course, now the whole program is incorrect! Because you have swapped references now your foo
tried to return reference to a local object which would be destroyed at the end of that function!
So, the summary:
-
Lifetimes are not, really, parts of your program, rather they are something you tell the compiler.
-
And what you tell the compiler is, essentially, the proof of correctness.
-
That story is both for the compiler and for the readers of your code. It should explain how your code behaves.
Both reader and compiler have to “understand” how your data structures work by just looking of signatures of functions and declaration of data structures.
-
While reader of your code can understand your code for real the compiler couldn't. But it can verify that your annotations include a valid proof of correcntess.
You should always keep in mind than compiler is certifiable loon, it's insane… by definition. Simply because it doesn't have any organs which may make it sane. It's all too hard to believe in that because, especially with Rust compiler, error messages are so sane… but they are sane because computer developers are sane, not because compiler is sane! Any compiler for any language is insane… don't forget that.