Every Rust reference has a lifetime, even when its type is written without
an explicit name such as 'a. You usually do not need to name it. Rust uses
lifetime elision rules in signatures and inference within function bodies
to check how references are used.
An explicit annotation describes a relationship the compiler must check. It does not keep data alive or change who owns it.
Start with a familiar function
fn trim(s: &str) -> &str {
s.trim()
}
There is one input reference lifetime. The elision rules assign that lifetime to the output, so the signature expresses the same relationship as:
fn trim<'a>(s: &'a str) -> &'a str {
s.trim()
}
The result is a slice of the input text. Its use must fit within a borrow of the input; the function does not allocate a new string for the result. The caller can choose inputs with different lifetimes on different calls.
Why an annotation can be necessary
This signature has two distinct elided input lifetimes, so the ordinary function elision rules cannot choose one for its output:
fn pick(a: &str, _b: &str) -> &str {
a
}
It produces E0106. Even though the body returns a, signature elision does
not inspect the body to establish the public relationship. If the function
should return a slice from a, state that:
fn pick<'a>(a: &'a str, _b: &str) -> &'a str {
a
}
If either input may be returned, a common signature gives both inputs and the output one lifetime parameter. That lets the caller borrow both inputs for a region in which the result can be used. It does not require the two owners to have identical creation or destruction times.
Methods have another elision rule: when the receiver is &self or
&mut self, its lifetime is assigned to elided output lifetimes. Thus,
multiple reference inputs do not always mean explicit annotations are needed.
Separate scope, storage and borrow validity
Three related ideas help explain borrow errors:
- A binding's scope determines where its name can be used in source code.
- An owned value's storage duration concerns when its storage exists. Moving, dropping or replacing a value can matter even within one scope.
- A reference's lifetime, also called a region, describes where that reference must be valid. The borrow checker checks the constraints imposed by its uses, including references derived from it.
The referent is what a reference points to. In let r = &s, it is the
value stored in s. In a string slice such as &text[..5], it is the
selected string data, not the String handle itself.
fn main() {
let text = String::from("hello world");
let first = &text[..5];
println!("{first}");
let mut owned = text;
owned.push('!');
println!("{owned}");
}
Here the borrow is no longer needed after the first print. Although the
name first remains in scope, moving text afterward is allowed. If you
use first again after that move, the compiler rejects the conflict.
Other situations, such as guards with destructors, can keep access active
until a value is dropped; do not apply a last-read rule blindly to every type.
Check your understanding Why can text move while first is still in scope? Show answer Hide answer
Scope alone does not determine how long a borrow is needed. In this example,
the last use of first occurs before the move. No later operation needs the
slice, so the borrow does not prevent moving the owning String.
What the checking protects
A reference must refer to valid data wherever it is used. Borrow checking also enforces compatible access, including restrictions on overlapping shared and mutable borrows. It does more than compare destruction times.
Lifetime parameters describe these relationships at compile time. They do not add a runtime lifetime counter to each reference. Reference representation still depends on the pointed-to type: references to sized values carry a pointer, while slices and trait objects also carry metadata such as a length or a vtable pointer. Lifetime syntax does not add that metadata.
This does not mean every safe Rust abstraction has no runtime checks.
For example, RefCell checks borrow state at runtime and slice indexing
can check bounds. These are separate mechanisms.
Check the intended signature with callers
Runtime assertions test behavior, but cannot query a lifetime parameter.
Compile-time checks can also test whether an API accepts its intended uses.
For example, this caller should work with the pick function above:
fn pick<'a>(a: &'a str, _b: &str) -> &'a str { a }
fn main() {
let a = String::from("keep");
let result;
{
let b = String::from("temporary");
result = pick(&a, &b);
}
assert_eq!(result, "keep");
}
Tying b to the output lifetime unnecessarily would reject this caller.
That is a useful API test even though both versions might pass runtime tests
whose input owners all remain alive until the end of the test.
An annotation does not extend storage duration
Writing &'static str requires a reference valid for the static lifetime;
it does not turn a borrowed local String into permanent storage. A string
literal can meet that requirement. A reference into a local string that will
be dropped cannot meet it merely because the return type says 'static.
Keep this separate from T: 'static, a type bound. An owned String
satisfies that bound and may still be dropped normally. The bound restricts
borrowed data a type can contain; it does not require every value of that
type to live forever.
When an annotation fails, identify the owner, the reference's uses and the relationship the signature promises. Those facts tell you whether to change the signature, shorten a borrow or change where the data is owned.