Skip to content

Instantly share code, notes, and snippets.

@abitofhelp
Last active October 11, 2025 01:36
Show Gist options
  • Select an option

  • Save abitofhelp/f622cb0cd7f2de618b4c53f4a9d3b2af to your computer and use it in GitHub Desktop.

Select an option

Save abitofhelp/f622cb0cd7f2de618b4c53f4a9d3b2af to your computer and use it in GitHub Desktop.

Revisions

  1. abitofhelp revised this gist Oct 11, 2025. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion series-part-3-rule-3.md
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    # Understanding Rust Lifetime Elision - Part 3:
    # Understanding Rust Lifetime Elision - Part 3 of 4:
    # Rule 3 - The Method Receiver Rule

    **Part 3 of 4: A comprehensive guide to mastering Rust's lifetime elision rules**
  2. abitofhelp revised this gist Oct 11, 2025. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions series-part-3-rule-3.md
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,4 @@
    # Understanding Rust Lifetime Elision - Part 3:
    # Rule 3 - The Method Receiver Rule

    **Part 3 of 4: A comprehensive guide to mastering Rust's lifetime elision rules**
  3. abitofhelp created this gist Oct 11, 2025.
    476 changes: 476 additions & 0 deletions series-part-3-rule-3.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,476 @@
    # Rule 3 - The Method Receiver Rule

    **Part 3 of 4: A comprehensive guide to mastering Rust's lifetime elision rules**

    ---

    ## 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

    ---

    ## 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:

    ```rust
    /// 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:**

    1. `&self` has the lifetime `'a` (from `Book<'a>`)
    2. Rule 3 sees a method with `&self`
    3. The return type `&str` automatically 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:

    ```rust
    /// 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`) and `marker` (lifetime `'b`)
    - Rule 2 doesn't apply (multiple inputs)
    - Rule 3 kicks in: the return value gets `&self`'s lifetime (`'a`), NOT `marker`'s lifetime (`'b`)
    - This makes sense—we're returning a slice of `self.text`, not of `marker`

    ---

    ## Complex Example

    Let's examine a more sophisticated case with multiple parameters and complex return types:

    ```rust
    /// 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 `&User` and `&str` in 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`:

    ```rust
    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:

    ```rust
    /// 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:

    ```rust
    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`) and `pattern` (`'b`)
    - TWO owned parameters: `start_pos` and `case_sensitive` (ignored)
    - Return type gets `'a` (from `&self`), NOT `'b` (from `pattern`)

    ---

    ## 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`)

    ```rust
    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

    ```rust
    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

    1. **Rule 3 only applies to methods** (functions in `impl` blocks with `&self` or `&mut self`)
    2. **Return values get `&self`'s lifetime**, not other parameters' lifetimes
    3. **Owned parameters are ignored** when determining lifetimes
    4. **Works with both `&self` and `&mut self`**
    5. **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 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

    ---

    ## Practice Exercise

    For each method, determine if Rule 3 applies and what lifetime the return value gets:

    ```rust
    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 }
    }
    ```

    <details>
    <summary>Click to see answers</summary>


    - **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. `x` and `y` are owned (ignored). Return gets `'a`

    All three methods return references to `self.data`, so they all get `&self`'s lifetime `'a`.

    </details>

    ---

    **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!*