Alright, let’s talk about the three flavors of self you’ll find in method signatures. This isn’t just academic syntax wankery; the choice of receiver is you, the programmer, telling the compiler exactly what you intend to do with the data. It’s a contract. Get it right, and the compiler becomes your best friend, ruthlessly enforcing your API design. Get it wrong, and you’ll be fighting the borrow checker until the heat death of the universe. Or at least until you fix it.

Think of a struct as a house. A method is you going to that house to do something. The type of self you use determines if you’re just peeking through the window, going inside to rearrange the furniture, or—dramatically—burning the place down for insurance money and taking the land.

&self: The Polite Visitor

This is the most common, and the most polite, receiver. You’re borrowing the struct immutably. You want to look at the data, maybe calculate something from it, but you absolutely promise not to change a single byte.

Why would you do this? Because it’s non-destructive. You can call this method as many times as you like, from as many places as you like, concurrently, without a care in the world. The data stays pristine.

#[derive(Debug)]
struct Kettle {
    temperature_c: u32,
    is_boiling: bool,
}

impl Kettle {
    // This method only needs to read data to tell you something.
    fn display_status(&self) -> String {
        format!("{}°C. Boiling: {}", self.temperature_c, self.is_boiling)
    }

    // A read-only method can even trigger logic based on the data...
    fn should_make_tea(&self) -> bool {
        self.is_boiling
    }

    // ...but it cannot change it. This won't compile:
    // fn cool_down(&self) {
    //     self.temperature_c -= 1; // ERROR: Cannot mutate data through an `&self` reference.
    // }
}

fn main() {
    let my_kettle = Kettle { temperature_c: 95, is_boiling: false };
    println!("{}", my_kettle.display_status()); // "95°C. Boiling: false"
    println!("Make tea? {}", my_kettle.should_make_tea()); // "Make tea? false"
}

The key takeaway: use &self for any method that doesn’t need to alter the instance itself. It’s the default choice for a getter or any pure function.

&mut self: The Interior Decorator

This is a mutable borrow. You’re saying, “I need to get inside this struct and change it.” This gives you exclusive access. While you’re in there messing with the furniture, no one else can be looking in the windows (&self) or also trying to redecorate (another &mut self). The borrow checker will see to that.

This is your go-to for methods that modify internal state.

impl Kettle {
    // This needs to change the struct's data.
    fn heat_up(&mut self) {
        self.temperature_c += 10;
        // Now we update the boiling state based on the new temperature.
        self.is_boiling = self.temperature_c >= 100;
    }
}

fn main() {
    let mut my_kettle = Kettle { temperature_c: 95, is_boiling: false }; // Note: `mut`!
    println!("Before: {}", my_kettle.display_status()); // "Before: 95°C. Boiling: false"

    my_kettle.heat_up(); // MUTATES the struct
    println!("After: {}", my_kettle.display_status()); // "After: 105°C. Boiling: true"
}

See the mut on my_kettle? That’s crucial. You can’t call a &mut self method on a variable that isn’t declared as mutable. The method signature is enforcing good habits at the call site.

self: The Arsonist

This is ownership. The method consumes the entire struct by value. You’re saying, “I’m taking this data, and I might do something that ends it, transform it into something else entirely, or just prevent anyone else from using it ever again.”

This is less common but incredibly powerful. It’s used for:

  1. Transformers: Methods that take ownership and return a new, transformed value (e.g., into_vec).
  2. Destructors: The infamous drop method takes self by value, literally ending its lifetime.
  3. Chaining Finalizers: When you want to signal that this is the last operation in a chain.
impl Kettle {
    // This is the end of the line for this Kettle instance.
    fn dismantle(self) -> (u32, bool) {
        // We take ownership of `self` and break it into its parts.
        // The original `my_kettle` is GONE after this.
        (self.temperature_c, self.is_boiling)
    }
}

fn main() {
    let mut my_kettle = Kettle { temperature_c: 105, is_boiling: true };
    let (temp, boiling) = my_kettle.dismantle(); // `my_kettle` is moved and consumed.

    // println!("{}", my_kettle.display_status()); // ERROR: Use of moved value!
    println!("Salvaged parts: Temp={}, Boiling={}", temp, boiling);
}

The beauty of this is its finality. The type system guarantees that after calling dismantle(), the original object is gone and can no longer be used accidentally. It’s perfect for signaling a “final” operation or for extracting owned data from inside a wrapper without needing to clone it.

Choosing the right receiver is the cornerstone of designing a clear and compiler-verified API. &self for looky-loos, &mut self for changers, and self for terminators. Use them wisely.