Structs
self, &self, and &mut self
Every method's first parameter specifies how the method receives the instance it is called on. Rust provides three primary shorthand receivers—&self (shared borrow), &mut self (mutable borrow), and self (by-value ownership)—along with explicit smart-pointer receivers such as self: Box<Self> and self: Arc<Self>.
Comparing Method Receivers
| Shorthand Syntax | Expanded Parameter Type | Ownership Effect on Caller's Instance | When to Use |
|---|---|---|---|
&self |
self: &Self |
Borrows immutably; caller keeps the instance | Reading fields, computing derived values, formatting, or inspecting state |
&mut self |
self: &mut Self |
Borrows mutably; caller keeps the instance | Updating fields in place, clearing buffers, or advancing an iterator |
self |
self: Self |
Moves (consumes) the instance (unless Self: Copy) |
Transforming the value into another type (into_*), closing a resource, or chaining builder steps |
mut self |
mut self: Self |
Moves the instance and binds self as mutable locally |
Builder methods that mutate self locally before returning self by value |
self: Box<Self> |
self: Box<Self> |
Moves a heap-boxed instance (Box<Self>) |
Consuming a boxed trait object (Box<dyn Trait>) or large heap node |
self: Arc<Self> |
self: Arc<Self> |
Clones or moves an Arc<Self> handle |
Spawning background tasks that need a shared Arc<Self> clone |
Lifecycle of `item` Across Receiver Types:
item.inspect() (&self) ──> Read-only borrow; `item` remains valid and unchanged
item.modify() (&mut self) ──> Exclusive borrow; `item` remains valid with updated fields
item.into_inner() (self) ──> Ownership moved into method; `item` is consumed!
All Three Primary Receivers in Action
#[derive(Debug)]
struct StringBuffer {
content: String,
}
impl StringBuffer {
fn new(initial: &str) -> Self {
Self {
content: String::from(initial),
}
}
// 1. `&self`: read-only inspection
fn byte_len(&self) -> usize {
self.content.len()
}
// 2. `&mut self`: in-place mutation
fn append_line(&mut self, line: &str) {
if !self.content.is_empty() {
self.content.push('\n');
}
self.content.push_str(line);
}
// 3. `self`: consumes `StringBuffer` and returns the inner `String` without cloning
fn into_string(self) -> String {
self.content
}
}
fn main() {
let mut buf = StringBuffer::new("Header");
println!("Initial length (&self): {}", buf.byte_len());
buf.append_line("Body line 1"); // Calls `&mut self`
buf.append_line("Body line 2");
println!("Updated length (&self): {}", buf.byte_len());
// Consume `buf` by value (`self`) to extract the owned `String`:
let final_text: String = buf.into_string();
println!("Extracted text:\n{final_text}");
// `buf` can no longer be used here because `into_string(self)` consumed it!
}
Initial length (&self): 6
Updated length (&self): 30
Extracted text:
Header
Body line 1
Body line 2
Using mut self for Chained Value Builders
Notice the difference between &mut self (a mutable reference to the caller's variable) and mut self (taking ownership of self by value and allowing local mutation inside the method body):
#[derive(Debug)]
struct Request {
path: String,
timeout_ms: u64,
}
impl Request {
fn get(path: &str) -> Self {
Self {
path: String::from(path),
timeout_ms: 1000,
}
}
// Takes ownership via `mut self`, modifies the field, and returns `Self`:
fn with_timeout(mut self, timeout_ms: u64) -> Self {
self.timeout_ms = timeout_ms;
self
}
}
fn main() {
// No `let mut` needed at the call site because `with_timeout` moves and returns `Self`:
let req = Request::get("/api/v1/status").with_timeout(5000);
println!("{req:?}");
}
Request { path: "/api/v1/status", timeout_ms: 5000 }
Smart-Pointer Receivers (self: Box<Self>, self: Arc<Self>, self: Pin<&mut Self>)
Rust allows explicit type annotations on self when the type is a wrapper around Self:
self: &Self(identical to&self)self: &mut Self(identical to&mut self)self: Box<Self>self: Rc<Self>self: Arc<Self>self: Pin<&Self>orself: Pin<&mut Self>
use std::sync::Arc;
struct Worker {
name: String,
}
impl Worker {
// Requires the caller to invoke this method on an `Arc<Worker>`:
fn spawn_label(self: &Arc<Self>) -> Arc<Self> {
Arc::clone(self)
}
}
fn main() {
let shared = Arc::new(Worker {
name: String::from("indexer-1"),
});
let handle = shared.spawn_label();
println!(
"Worker '{}' strong count: {}",
handle.name,
Arc::strong_count(&shared)
);
}
Worker 'indexer-1' strong count: 2