Part 3 of 4: A comprehensive guide to mastering Rust's lifetime elision rules
- Part 1: Introduction and Rule 1
- Part 2: Rule 2 - The Single Input Lifetime Rule
- Part 3: Rule 3 - The Method Receiver Rule ← You are here
- Part 4: Structs, Lifetimes, and When Elision Doesn't Apply
- Rule 1: Each reference parameter gets its own lifetime
- Rule 2: If there's exactly one reference input, all outputs get that lifetime
Now we tackle the most nuanced rule—the one that handles methods on structs.
If there are multiple input lifetime parameters, but one of them is &self or &mut self, the lifetime of self is assigned to all output lifetime parameters.
This rule is specifically for methods (functions defined in an impl block). It resolves the ambiguity when you have multiple reference inputs by always preferring &self's lifetime for the output.
When you call a method on an object, you're usually interested in data from that object, not from the other parameters. Rule 3 encodes this common pattern: method return values typically reference the receiver (self), not the arguments.
Let's start with a basic getter method:
/// Book data container
///
/// # Fields
/// * `title` - Reference to book title with lifetime 'a
///
/// Note: Struct fields must always have explicit lifetime annotations.
struct Book<'a> {
title: &'a str, // Must explicitly declare lifetime 'a
}
impl<'a> Book<'a> {
/// Gets the book's title
/// Rule 3 applies: return gets &self's lifetime
///
/// # Parameters
/// * `&self` - Reference to the Book instance
///
/// # Returns
/// * `&str` - Reference to the title string
fn get_title(&self) -> &str {
self.title
}
/// Gets the book's title (explicit lifetimes)
///
/// # Parameters
/// * `&self` - Reference to the Book instance
///
/// # Returns
/// * `&'a str` - Reference to title with Book's lifetime 'a
fn get_title_explicit(&self) -> &'a str {
self.title
}
}What's happening:
&selfhas the lifetime'a(fromBook<'a>)- Rule 3 sees a method with
&self - The return type
&strautomatically gets lifetime'a
This is a simple getter—we're returning data that belongs to self.
Now let's look at a method with multiple reference parameters:
/// Parser for text data
///
/// # Fields
/// * `text` - Reference to text being parsed with lifetime 'a
///
/// Note: Struct fields must always have explicit lifetime annotations.
struct Parser<'a> {
text: &'a str, // Must explicitly declare lifetime 'a
}
impl<'a> Parser<'a> {
/// Finds text after a marker
/// Rule 3 applies: multiple inputs (self + marker), but return gets &self's lifetime
///
/// # Parameters
/// * `&self` - Reference to the Parser instance
/// * `marker` - String reference to search for
///
/// # Returns
/// * `&str` - Reference to text after marker, or empty string
fn find_after(&self, marker: &str) -> &str {
if let Some(pos) = self.text.find(marker) {
&self.text[pos..]
} else {
""
}
}
/// Finds text after a marker (explicit lifetimes)
///
/// # Parameters
/// * `&self` - Reference to the Parser instance
/// * `marker` - String reference with lifetime 'b
///
/// # Returns
/// * `&'a str` - Reference to text with Parser's lifetime 'a (NOT 'b)
fn find_after_explicit<'b>(&self, marker: &'b str) -> &'a str {
if let Some(pos) = self.text.find(marker) {
&self.text[pos..]
} else {
""
}
}
}Critical insight:
- We have TWO reference inputs:
&self(lifetime'a) andmarker(lifetime'b) - Rule 2 doesn't apply (multiple inputs)
- Rule 3 kicks in: the return value gets
&self's lifetime ('a), NOTmarker's lifetime ('b) - This makes sense—we're returning a slice of
self.text, not ofmarker
Let's examine a more sophisticated case with multiple parameters and complex return types:
/// Database with user data
///
/// # Fields
/// * `users` - Reference to user slice with lifetime 'a
/// * `cache` - Reference to string cache with lifetime 'a
///
/// Note: Struct fields must always have explicit lifetime annotations.
struct Database<'a> {
users: &'a [User], // Must explicitly declare lifetime 'a
cache: &'a [String], // Must explicitly declare lifetime 'a
}
struct User {
name: String,
id: u32,
}
impl<'a> Database<'a> {
/// Looks up a user by query string
/// Rule 3 applies: multiple inputs (self + query + filters), but return gets &self's lifetime
///
/// # Parameters
/// * `&self` - Reference to the Database instance
/// * `query` - String reference to search for
/// * `filters` - Slice reference containing filter strings
///
/// # Returns
/// * `Result<&User, &str>` - Ok with User reference, or Err with error message
fn lookup(
&self,
query: &str,
filters: &[&str]
) -> Result<&User, &str> {
self.users.iter()
.find(|u| u.name == query)
.ok_or("not found")
}
/// Looks up a user by query string (explicit lifetimes)
///
/// # Parameters
/// * `&self` - Reference to the Database instance
/// * `query` - String reference with lifetime 'b
/// * `filters` - Slice reference with lifetime 'c
///
/// # Returns
/// * `Result<&'a User, &'a str>` - Both variants have Database's lifetime 'a (NOT 'b or 'c)
fn lookup_explicit<'b, 'c>(
&self,
query: &'b str,
filters: &'c [&str]
) -> Result<&'a User, &'a str> {
self.users.iter()
.find(|u| u.name == query)
.ok_or("not found")
}
}What's happening:
- THREE reference inputs:
&self('a),query('b),filters('c) - Rule 3 applies: both
&Userand&strin the return type get lifetime'a - We're returning references to data in
self.users, so this makes perfect sense
Rule 3 works the same way with &mut self:
impl<'a> Database<'a> {
/// Gets cached string by index with mutable access
/// Rule 3 applies with &mut self
///
/// # Parameters
/// * `&mut self` - Mutable reference to the Database instance
/// * `index` - Reference to the index value
///
/// # Returns
/// * `Option<&str>` - Some with string reference, or None if index out of bounds
fn get_cache_mut(&mut self, index: &usize) -> Option<&str> {
self.cache.get(*index).map(|s| s.as_str())
}
/// Gets cached string by index with mutable access (explicit lifetimes)
///
/// # Parameters
/// * `&mut self` - Mutable reference to the Database instance
/// * `index` - Reference to index with lifetime 'b
///
/// # Returns
/// * `Option<&'a str>` - Option containing reference with Database's lifetime 'a (NOT 'b)
fn get_cache_mut_explicit<'b>(&mut self, index: &'b usize) -> Option<&'a str> {
self.cache.get(*index).map(|s| s.as_str())
}
}Key point: Whether &self or &mut self, Rule 3 still applies—the return gets self's lifetime.
Just like Rules 1 and 2, owned parameters don't affect Rule 3:
/// Data storage with content and metadata
///
/// # Fields
/// * `content` - Reference to byte content with lifetime 'a
/// * `name` - Reference to name string with lifetime 'a
///
/// Note: Struct fields must always have explicit lifetime annotations.
struct DataStore<'a> {
content: &'a [u8], // Must explicitly declare lifetime 'a
name: &'a str, // Must explicitly declare lifetime 'a
}
impl<'a> DataStore<'a> {
/// Gets a slice of content with bounds checking
/// Rule 3 applies: owned params (offset, length, validate) don't affect lifetime elision
///
/// # Parameters
/// * `&self` - Reference to the DataStore instance
/// * `offset` - Starting position (owned, no lifetime)
/// * `length` - Number of bytes to extract (owned, no lifetime)
/// * `validate` - Whether to validate bounds (owned, no lifetime)
///
/// # Returns
/// * `&[u8]` - Reference to the content slice
fn get_slice(&self, offset: usize, length: usize, validate: bool) -> &[u8] {
if validate && offset + length > self.content.len() {
&[]
} else {
&self.content[offset..offset + length]
}
}
/// Gets a slice of content with bounds checking (explicit lifetimes)
///
/// # Parameters
/// * `&self` - Reference to the DataStore instance
/// * `offset` - Starting position (owned, no lifetime)
/// * `length` - Number of bytes to extract (owned, no lifetime)
/// * `validate` - Whether to validate bounds (owned, no lifetime)
///
/// # Returns
/// * `&'a [u8]` - Reference to content with DataStore's lifetime 'a
fn get_slice_explicit(
&self,
offset: usize,
length: usize,
validate: bool
) -> &'a [u8] {
if validate && offset + length > self.content.len() {
&[]
} else {
&self.content[offset..offset + length]
}
}
}What's happening: Four parameters total, but only &self is a reference. The owned parameters (offset, length, validate) are ignored for lifetime purposes.
Here's a method with both reference and owned parameters:
impl<'a> DataStore<'a> {
/// Searches for a pattern in the datastore
/// Rule 3 applies: pattern is a reference, but return gets &self's lifetime
///
/// # Parameters
/// * `&self` - Reference to the DataStore instance
/// * `pattern` - String reference to search for
/// * `start_pos` - Starting search position (owned, no lifetime)
/// * `case_sensitive` - Whether search is case-sensitive (owned, no lifetime)
///
/// # Returns
/// * `Option<&str>` - Some with reference to name, or None
fn search(
&self,
pattern: &str,
start_pos: usize,
case_sensitive: bool
) -> Option<&str> {
Some(self.name)
}
/// Searches for a pattern in the datastore (explicit lifetimes)
///
/// # Parameters
/// * `&self` - Reference to the DataStore instance
/// * `pattern` - String reference with lifetime 'b
/// * `start_pos` - Starting search position (owned, no lifetime)
/// * `case_sensitive` - Whether search is case-sensitive (owned, no lifetime)
///
/// # Returns
/// * `Option<&'a str>` - Option containing reference with DataStore's lifetime 'a (NOT 'b)
fn search_explicit<'b>(
&self,
pattern: &'b str,
start_pos: usize,
case_sensitive: bool
) -> Option<&'a str> {
Some(self.name)
}
}Critical distinction:
- TWO reference parameters:
&self('a) andpattern('b) - TWO owned parameters:
start_posandcase_sensitive(ignored) - Return type gets
'a(from&self), NOT'b(frompattern)
Rule 3 doesn't always work. You need explicit lifetimes when:
impl<'a> Parser<'a> {
// This won't compile with elision - we're returning `other`, not `self`
fn choose<'b>(&self, other: &'b str, use_other: bool) -> &'b str {
if use_other {
other // Returning the parameter, not self!
} else {
"" // Can't return self.text here
}
}
}impl<'a> Parser<'a> {
// Need explicit lifetimes to specify which reference we return
fn select<'b>(&'a self, alternative: &'b str, prefer_alt: bool) -> &'b str
where
'a: 'b // Require 'a outlives 'b
{
// Complex logic here...
alternative
}
}- Rule 3 only applies to methods (functions in
implblocks with&selfor&mut self) - Return values get
&self's lifetime, not other parameters' lifetimes - Owned parameters are ignored when determining lifetimes
- Works with both
&selfand&mut self - Encodes a common pattern: methods typically return references to the receiver's data
In Part 4, we'll explore Structs, Lifetimes, and When Elision Doesn't Apply. We'll dive deep into:
- Why struct fields must have explicit lifetimes
- Cases where elision fails and you need explicit annotations
- Advanced patterns and best practices
- How to debug lifetime errors
For each method, determine if Rule 3 applies and what lifetime the return value gets:
struct Container<'a> {
data: &'a str,
}
impl<'a> Container<'a> {
fn method_a(&self) -> &str { self.data }
fn method_b(&self, other: &str) -> &str { self.data }
fn method_c(&self, x: usize, y: usize) -> &str { self.data }
}Click to see answers
- method_a: ✅ Rule 3 applies. Return gets
'a(from&self) - method_b: ✅ Rule 3 applies. Return gets
'a(from&self), NOTother's lifetime - method_c: ✅ Rule 3 applies.
xandyare owned (ignored). Return gets'a
All three methods return references to self.data, so they all get &self's lifetime 'a.
Series Navigation:
- Part 1: Introduction and Rule 1
- Part 2: Rule 2 - The Single Input Lifetime Rule
- Part 3: Rule 3 - The Method Receiver Rule ← You are here
- Part 4: Structs, Lifetimes, and When Elision Doesn't Apply
This article is part of a comprehensive educational series on Rust lifetime elision. Continue to Part 4 to complete your understanding!