A method returning a reference needs to say what keeps that reference valid. Often the receiver is the right answer. A view over someone else's data can sometimes offer a more flexible contract.
With &self or &mut self, Rust's method elision rule assigns the receiver
borrow's lifetime to elided output lifetime parameters. This includes a
reference inside Option, not just a bare reference return. Explicit output
lifetimes let you express a different relationship. See the
lifetime-elision reference.
When the receiver owns the data
This complete example returns a slice of a String owned by Owner:
struct Owner { text: String }
impl Owner {
fn text(&self) -> &str { &self.text }
}
fn main() {
let owner = Owner { text: "hello".to_string() };
assert_eq!(owner.text(), "hello");
}
The caller must keep the owner alive and cannot mutate its String in a way that conflicts with a still-used slice. The borrow protects against both destruction and changes such as replacing or reallocating the String. Elision expresses that relationship without extra annotations.
When the receiver borrows the data
Viewer stores a shared reference to text it does not own. The following complete program intentionally fails with E0597:
struct Viewer<'a> { rest: &'a str }
impl Viewer<'_> {
fn peek(&self) -> Option<&str> {
self.rest.split_whitespace().next()
}
}
fn main() {
let source = String::from("a b");
let first;
{
let viewer = Viewer { rest: &source };
first = viewer.peek();
}
assert_eq!(first, Some("a"));
}
peek's elided result is tied to the borrow of viewer. The final assertion uses it after viewer's scope ends. The compiler correctly rejects that use under the declared signature, even though source is still alive.
The method itself is sound. If callers only need the result while borrowing viewer, it can be an acceptable API. If they should retain the result after viewer goes away, name the lifetime of its stored shared reference instead:
struct Viewer<'a> { rest: &'a str }
impl<'a> Viewer<'a> {
fn peek(&self) -> Option<&'a str> {
self.rest.split_whitespace().next()
}
}
const _: () = {
fn _pin<'a>(viewer: &Viewer<'a>) -> Option<&'a str> {
viewer.peek()
}
};
fn main() {
let source = String::from("a b");
let first;
{
let viewer = Viewer { rest: &source };
first = viewer.peek();
}
assert_eq!(first, Some("a"));
}
The result now borrows the source text independently of the receiver borrow. This does not extend the source's lifetime: the source must remain valid for every use of first. The annotation describes that requirement rather than changing when anything is allocated or destroyed.
Test how callers can use the result
The _pin function above requires a result with the source lifetime even when the receiver is borrowed for a shorter time. Its body is type-checked without being called. Changing peek back to an elided result makes that check fail with E0621, asking for a longer borrow of viewer.
This is one way to test an API's lifetime contract. A unit test, integration test or runnable documentation example can also put viewer in an inner scope and use the result afterwards. Such tests must compile before their runtime assertions run. Merely checking returned words while viewer remains borrowed would not distinguish the two signatures.
Compilation checks the relationships you express; it cannot choose which caller behavior your API should promise. Include that behavior in tests.
Returning slices while advancing a cursor
A mutable receiver makes the distinction useful for parsers. This complete cursor example takes up to n remaining bytes and advances its position:
struct Cursor<'a> { data: &'a [u8], pos: usize }
impl<'a> Cursor<'a> {
fn remaining(&self) -> &'a [u8] {
&self.data[self.pos..]
}
fn position(&self) -> usize { self.pos }
fn take(&mut self, n: usize) -> &'a [u8] {
let start = self.pos;
let count = n.min(self.data.len() - start);
self.pos += count;
&self.data[start..self.pos]
}
}
fn main() {
let data = [10, 20, 30];
let (first, second);
{
let mut cursor = Cursor { data: &data, pos: 0 };
first = cursor.take(1);
second = cursor.take(usize::MAX);
assert_eq!(cursor.position(), 3);
assert!(cursor.remaining().is_empty());
}
assert_eq!(first, &[10]);
assert_eq!(second, &[20, 30]);
}
Starting at position zero, take maintains pos <= data.len(). Oversized
requests consume only the remaining bytes, and a zero request returns an
empty slice without advancing. It performs no text decoding or byte copying.
If take returned an elided &[u8], its output would keep the mutable receiver
borrow active while that output is still used. Keeping first for the later
assertion would then prevent the second mutable borrow, producing E0499.
The explicit source lifetime allows both results to remain usable while the
cursor advances. position returns an owned usize and needs no output lifetime.
Decide from the data and the intended API
Ask where the returned reference points and whether callers need it to remain usable independently of the receiver. For Owner's String, the receiver borrow is appropriate. For this Viewer's shared source text, naming the source lifetime enables additional safe uses.
Do not apply that change mechanically to every struct containing references. A method might return data from its own scratch buffer, or hold a mutable reference whose borrowing rules require a different design. The body must be able to satisfy the signature you choose.
Writing fn text<'a>(&'a self) -> &'a str for Owner only repeats what elision
already means. Clippy's needless_lifetimes lint warns about that redundant
annotation, and warnings-as-errors makes it fail the style check. Viewer's
explicit source lifetime is meaningful because elision would change its
contract.