Understanding Rust Lifetime Elision Rule 3: The Method Receiver
Part 3 of 4: A comprehensive guide to mastering Rust’s lifetime elision rules
Understanding Rust Lifetime Elision Rule 3: The Method Receiver
Part 3 of 4: A comprehensive guide to mastering Rust’s lifetime elision rules

Series Navigation
- Rule 1 — Introduction
- Rule 2 — The Single Input Lifetime
- Rule 3 — The Method Receiver← You are here
- Rule 4 -Structs, Lifetimes, and When Elision Doesn’t Apply
Recap from Parts 1 and 2
- 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.
Rule 3: Method Receiver Rule
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.
Why This Rule Exists
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.
Simple Example
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.
Intermediate Example
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.
Complex Example
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 with Mutable References
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.
Rule 3 with Non-Reference Parameters
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.
Mixed Reference and Owned Parameters
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)
When Would You Need Explicit Lifetimes?
Rule 3 doesn’t always work. You need explicit lifetimes when:
1. Returning a reference to a parameter (not self)
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
}
}
}
2. Returning references with mixed lifetimes
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
}
}
Key Takeaways for Rule 3
- 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.
What’s Next?
In Part 4, we’ll explore Structs, Lifetimes, and When Elision Doesn’t Apply. We’ll dive deep into:
- Why must struct fields have explicit lifetimes?
- Cases where elision fails and you need explicit annotations.
- Advanced patterns and best practices.
- How to debug lifetime errors.
Practice Exercise
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 }
}
Answers
- method_a: ✅ Rule 3 applies. Return gets
'a(from&self) - method_b: ✅ Rule 3 applies. Return gets
'a(from&self), NOT `other'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:
- Rule 1: Introduction
- Rule 2 — The Single Input Lifetime
- Rule 3 — The Method Receiver ← You are here
- Rule 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!
메타데이터
- post_id
- beb206fa9767
- slug
- understanding-rust-lifetime-elision-rule-3-the-method-receiver-beb206fa9767
- url
- https://medium.com/@abitofhelp/understanding-rust-lifetime-elision-rule-3-the-method-receiver-beb206fa9767
- canonical_url
- https://medium.com/@abitofhelp/understanding-rust-lifetime-elision-rule-3-the-method-receiver-beb206fa9767
- author_url
- https://medium.com/@abitofhelp
- status
- ok
- fetched_at
- 2026-07-16 19:42:55