Alright, let’s get our hands dirty. You’ve defined a struct, a beautiful little collection of data. It’s sitting there, inert, like a recipe without a chef. An impl block is how you hand that recipe to a chef and say, “Here, now make this, do that, bring it to life.” It’s where we attach the behavior—the methods—to our data structure.

Think of it this way: data is nouns, methods are the verbs. Your struct User { name: String } is the noun. A method like user.introduce() is the verb. The impl block is the sentence that connects them.

Here’s the basic syntax. It’s deceptively simple, which is where most of the magic lies.

struct User {
    name: String,
    email: String,
    sign_in_count: u64,
}

// Here's our impl block for the User struct
impl User {
    // This is a method. Its first parameter is always `self`.
    fn introduce(&self) -> String {
        format!("Hi, I'm {}! Contact me at {}.", self.name, self.email)
    }

    // This is an associated function (notice: no `self` parameter).
    // It's namespaced under the struct, often used for constructors.
    fn new(name: String, email: String) -> Self {
        Self {
            name,
            email,
            sign_in_count: 0, // Brand new user, so start at zero.
        }
    }
}

fn main() {
    // Using the associated function 'new' to construct an instance.
    let user = User::new("Alice".to_string(), "alice@rust.org".to_string());

    // Calling a method on the instance.
    println!("{}", user.introduce()); // Prints: Hi, I'm Alice! Contact me at alice@rust.org.
}

The Three Faces of self

This is the most important part to get right. The first parameter in a method defines how it borrows the struct instance. Get this wrong, and the borrow checker will become your pedantic, unyielding nemesis.

  1. &self: You’re borrowing an immutable reference. This is for methods that read data from the struct but don’t need to change it. Like our introduce() method. It’s the most common one. Use this by default until you have a reason not to.
  2. &mut self: You’re borrowing a mutable reference. This is for methods that need to change the data inside the struct. The borrow checker will ensure you only do this when you have exclusive access.
  3. self: You’re taking ownership of the instance. This consumes the original struct. You use this for methods that transform the value, like into something else, or when you want to ensure no one can use the old value again. The Drop trait uses this, for example.
impl User {
    // Reads data, so &self.
    fn get_email(&self) -> &str {
        &self.email
    }

    // Modifies data, so &mut self.
    fn change_email(&mut self, new_email: String) {
        self.email = new_email;
    }

    // Takes ownership, consuming the struct. Often used for final actions or transformations.
    fn into_tuple(self) -> (String, String) {
        (self.name, self.email)
    }
}

fn main() {
    let mut user = User::new("Bob".to_string(), "bob@old.com".to_string());
    user.change_email("bob@new.com".to_string()); // Fine, user is mutable.

    let email_ref = user.get_email(); // Fine, immutable borrow.
    // user.change_email("oops@try.again".to_string()); // ERROR! Can't mutably borrow while `email_ref` is alive.

    let (name, email) = user.into_tuple(); // user is moved and gone now.
    // println!("{}", user.get_email()); // ERROR! user was consumed.
}

Associated Functions: The Static Side

Notice in the first example we had fn new(...) -> Self. It doesn’t take self as a parameter. This is called an associated function—it’s associated with the type itself, not a particular instance. You call it using the :: syntax (User::new).

The most famous example is String::from("hello"). It’s not a method on a string; it’s a function that creates one. These are your perfect tool for constructors, helpers, or namespacing any functionality that doesn’t logically require an existing instance.

Why This Separation is Genius

Rust didn’t invent this data/implementation separation, but it perfects it. By keeping the struct definition and its impl blocks separate, you achieve incredible clarity. The struct is a pure, dumb data declaration. All the logic is neatly organized in impl blocks. It also means you can add methods to a type you didn’t define (as long as you define the trait or the type is in your crate)—a concept called “extension traits” that we’ll cover later, which is wildly powerful.

A common pitfall? Forgetting the & on &self and accidentally moving the entire struct into the method, which is almost never what you want for a simple accessor. The compiler will catch this, but it’s a classic newbie moment. Embrace it. We’ve all been there. The other is trying to call a method that requires &mut self on an instance that isn’t declared as mut. The compiler’s error will guide you, but it’s a clear sign you need to think more carefully about your mutability requirements.

So, to sum up: impl blocks are where your data learns its tricks. Use &self to look, &mut self to touch, and self to consume. And use associated functions to build new things from scratch. It’s a beautifully direct system that gives you immense power while keeping the borrow checker firmly on your side.