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> or self: 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