rust-metodos-impl-self

Methods in Rust with impl, self, and Associated Functions

  • 5 min

A method is a function associated with a type that typically works with its data.

In the previous article we defined Structs, which are great for storing data. Data that doesn’t do anything is a bit boring, so let’s give them behavior.

If you come from C# or Java, you’re used to defining the class and, within the same braces, writing the properties (data) and methods (behavior).

In Rust, this is done separately.

  • Data is defined in the struct.
  • Behavior is defined in a block called impl (implementation).

This separation is very clean and allows adding behavior to a type even across different files. Today we’ll see how to define methods, how to modify the instance with them, and what “static functions” are in Rust.

Defining methods with impl

Let’s bring back a classic example because it’s the easiest to visualize: a Rectangle.

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}
Copied!

To define a function that belongs to this struct (for example, to calculate its area), we open an impl Rectangle block:

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}
Copied!

The special parameter: &self

Notice the first parameter of the area function: &self.

In Rust, a function is considered a method when its first parameter is some form of self.

  • self is shorthand for self: Self, while &self is equivalent to self: &Self.
  • Self (capital S) is an alias for the data type we are in (in this case, Rectangle).

By using &self, we are saying: “To execute this function, I need to borrow (read) the instance I’m working on”.

To call the method, we use the dot syntax (.), just like in other languages:

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area() // Rust passes &rect1 automatically
    );
}
Copied!

Where is the -> operator from C++? In C++, if you have a pointer, you use -> to call a method. In Rust, there is Automatic Referencing and Dereferencing. Rust knows that area expects a &self, so when you call rect1.area(), Rust automatically adds the & for you.

Types of self: reading, writing, and consuming

Just like with normal functions, methods are subject to the rules of Ownership. Depending on how we define the first parameter, the method will be able to do different things.

&self: immutable borrow

This is the most common (like in the area example).

  • Meaning: “I only want to read the struct’s data”.
  • Consequence: The instance remains valid after calling the method.

&mut self: mutable borrow

We use it when the method needs to change the instance’s data.

impl Rectangle {
    fn resize(&mut self, factor: u32) {
        self.width *= factor;
        self.height *= factor;
    }
}

fn main() {
    let mut rect1 = Rectangle { width: 10, height: 20 };
    rect1.resize(2); // Modifies rect1
}
Copied!

To call this method, the original variable (rect1) must have been declared as mutable (let mut).

self: consuming the instance

If we write self without the &, the method takes ownership of the instance.

  • Meaning: “Give me the whole instance. It’s mine now. I decide whether to destroy it or transform it”.
  • Consequence: The original variable becomes invalid after calling the method.

This is less common, but very useful for methods that transform data into a completely different type and we want to prevent the old data from being used.

impl Rectangle {
    fn destroy(self) {
        println!("The rectangle has been destroyed and freed");
    } // Here, self goes out of scope and is destroyed along with its resources
}
Copied!

Associated Functions

Inside the impl block, we can also define functions that do NOT take self as their first parameter.

Since they don’t have an instance to act upon, they are not methods, but associated functions of the type. They play a role similar to static methods in Java or C#.

The main use: constructors

Rust does not have a new keyword. To create instances, the convention is to define an associated function called new.

impl Rectangle {
    // Does not take &self. Returns a Rectangle (Self) instance
    fn new(width: u32, height: u32) -> Rectangle {
        Rectangle { width, height }
    }
}
Copied!

To call these functions, we don’t use the dot (.); we use the scope resolution operator ::.

fn main() {
    // StructName::AssociatedFunction
    let rect1 = Rectangle::new(30, 50);
}
Copied!

new is just a convention. You could call it create, build, or make_rectangle. But it’s good practice to use new, as it’s what every Rust programmer expects to find.

Multiple impl blocks

Rust allows you to have multiple impl blocks for the same struct. This is useful for organizing code (though it’s not mandatory).

impl Rectangle {
    fn area(&self) -> u32 { ... }
}

impl Rectangle {
    fn new(...) -> Rectangle { ... }
}
Copied!

This will make much more sense when we see Traits (interfaces), where we will implement different traits in separate blocks.

Choosing the right receiver

When writing functions inside an impl, choose the right receiver:

  1. fn function() -> ... (No self): Associated Function. Use it for constructors (new). Called with ::.
  2. fn method(&self): Read Method. 90% of cases. Reads data without modifying it.
  3. fn method(&mut self): Write Method. Modifies the instance. Requires the object to be mut.
  4. fn method(self): Consuming Method. Takes ownership and destroys the original instance.