Last active
October 11, 2025 01:36
-
-
Save abitofhelp/f622cb0cd7f2de618b4c53f4a9d3b2af to your computer and use it in GitHub Desktop.
Revisions
-
abitofhelp revised this gist
Oct 11, 2025 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,4 +1,4 @@ # 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** -
abitofhelp revised this gist
Oct 11, 2025 . 1 changed file with 1 addition and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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** -
abitofhelp created this gist
Oct 11, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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!*