[Rust Guide] 5.3. Methods on Structs

rust dev.to

5.3.1. What Is a Method?

Methods are similar to functions. They are also declared with the fn keyword, and they also have names, parameters, and return values. But methods are different from functions in a few ways:

  • Methods are defined in the context of a struct (or an enum or a trait object).
  • The first parameter of a method is always self, which represents the struct instance the method belongs to and is called on, similar to self in Python and this in JavaScript.

5.3.2. Practical Use of Methods

Let’s continue with an example from the previous article:

struct Rectangle {  
    width: u32,  
    length: u32,  
}  

fn main() {  
    let rectangle = Rectangle{  
        width: 30,  
        length: 50,  
    };  
    println!("{}", area(&rectangle));  
}  

fn area(dim:&Rectangle) -> u32 {  
    dim.width * dim.length  
}
Enter fullscreen mode Exit fullscreen mode

The area function calculates an area, but it is special: it only applies to rectangles, not to other shapes or other types. If we later add functions that calculate the areas of other shapes, the name area will become ambiguous. Renaming it to rectangle_area would be cumbersome, because every call to this function in main would also need to be changed.

So if we could combine the Rectangle struct, which stores the rectangle’s width and length, with the area function, which only calculates a rectangle’s area, that would be ideal.

For this kind of requirement, Rust provides "implementation", whose keyword is impl. Follow it with the struct name and a pair of {} braces, and define methods inside just as you would define regular functions.

For this example, the struct name is Rectangle, so we can paste the code for the area function into the braces:

impl Rectangle {  
    fn area(dim:&Rectangle) -> u32 {  
        dim.width * dim.length  
    }  
}
Enter fullscreen mode Exit fullscreen mode

But note that this is not yet a method, because the first parameter of a method must be self. The code above is called an associated function, which will be covered below.

There is nothing wrong with writing it this way, but it can be simplified further. As mentioned above, the first parameter of a method is always self, so we can change it like this:

impl Rectangle {  
    fn area(&self) -> u32 {  
        self.width * self.length  
    }  
}
Enter fullscreen mode Exit fullscreen mode

Whichever type the method is bound to, self refers to that type. In this code, the area function is bound to Rectangle, so self refers to Rectangle. The area parameter does not need ownership, so we add & before self to indicate a reference.

Of course, after this change, the function call in main must also change—from a function call to a method call: instance.method_name(arguments).

fn main() {  
    let rectangle = Rectangle{  
        width: 30,  
        length: 50,  
    };  
    println!("{}", rectangle.area());  
}
Enter fullscreen mode Exit fullscreen mode

The parentheses in rectangle.area() are empty because the area method was defined using only &self as its parameter, which means the method borrows an immutable reference to self (that is, the rectangle instance). When calling area, you do not need to pass the instance explicitly, because the method call already knows implicitly that self is rectangle.

The full code is as follows:

struct Rectangle {  
    width: u32,  
    length: u32,  
}  

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

fn main() {  
    let rectangle = Rectangle{  
        width: 30,  
        length: 50,  
    };  
    println!("{}", rectangle.area());  
}
Enter fullscreen mode Exit fullscreen mode

Output:

1500
Enter fullscreen mode Exit fullscreen mode

5.3.3. How to Define Methods

We already did this in the practical example above, so here is just a summary:

  • Define methods inside impl
  • The first parameter of a method can be self, &self, or &mut self. It can take ownership, an immutable reference, or a mutable reference, just like other parameters.
  • Methods help organize code better, because methods for a type can all be placed inside the same impl block, so you do not have to search the entire codebase for behaviors related to a struct.

5.3.4. Operators for Method Calls

In C/C++, there are two operators for calling methods:

  • ->: The format is object->something(). Use this to call methods on the object pointed to by a pointer (that is, when object is a pointer).
  • .: The format is object.something(). Use this to call methods on the object itself (that is, when object is not a pointer, but an object).

object->something() is actually syntactic sugar. It is equivalent to (*object).something(), and * means dereference. In both cases, the process is to dereference first to get the object, and then call the method on that object.

Rust provides automatic referencing/dereferencing. In other words, when calling methods, Rust automatically adds &, &mut, or * as needed so that object matches the method signature. This is similar to Go.

For example, these two lines of code have the same effect:

point1.distance(&point2);
(&point1).distance(&point2);
Enter fullscreen mode Exit fullscreen mode

Rust will automatically add & before point1 when appropriate.

5.3.5. Method Parameters

In addition to self, methods can also take other parameters—one or more.

For example, based on the code in 5.3.2, we can add a feature that determines whether a rectangle can hold another rectangle (we will not consider rotated placement, and we will not consider the case where the rectangle’s length is greater than its width):

impl Rectangle {  
    fn can_hold(&self, other: &Rectangle) -> bool {  
        self.width > other.width && self.length > other.length  
    }  
}
Enter fullscreen mode Exit fullscreen mode

The logic is very easy to understand: as long as both the rectangle’s width and length are larger than the other rectangle’s, it works.

Then we can declare a few Rectangle instances in main and print the comparison result to see whether it works. The complete code is as follows:

struct Rectangle {  
    width: u32,  
    length: u32,  
}  

impl Rectangle {  
    fn can_hold(&self, other: &Rectangle) -> bool {  
        self.width > other.width && self.length > other.length  
    }  
}  

fn main() {  
    let rect1 = Rectangle{  
        width: 30,  
        length: 50,  
    };  
    let rect2 = Rectangle{  
        width: 10,  
        length: 40,  
    };  
    println!("{}", rect1.can_hold(&rect2));  
}
Enter fullscreen mode Exit fullscreen mode

Output:

true
Enter fullscreen mode Exit fullscreen mode

5.3.6. Associated Functions

You can define functions inside an impl block that do not take self as the first parameter. These are called associated functions (not methods). They are not called on an instance, but they are associated with the type. For example, String::from() is an associated function named from on the String type.

Associated functions are usually used as constructors, meaning they are used to create an instance of the associated type.

For example, based on the code in 5.3.2, we can add a constructor for a square (a square is also a special kind of rectangle):

impl Rectangle {  
    fn square(size: u32) -> Rectangle {  
        Rectangle{  
            width: size,  
            length: size,  
        }  
    }  
}
Enter fullscreen mode Exit fullscreen mode

Only one parameter is needed, because constructing a square only requires one side length.

Let’s try calling this associated function in main. The format is TypeName::function_name(arguments). The complete code is as follows:

#[derive(Debug)]  
struct Rectangle {  
    width: u32,  
    length: u32,  
}  

impl Rectangle {  
    fn square(size: u32) -> Rectangle {  
        Rectangle{  
            width: size,  
            length: size,  
        }  
    }  
}  

fn main() {  
    let square = Rectangle::square(10);  
    println!("{:?}", square);  
}
Enter fullscreen mode Exit fullscreen mode

Output:

Rectangle { width: 10, length: 10 }
Enter fullscreen mode Exit fullscreen mode

:: is not only used for associated functions; it is also used for modules to create namespaces (this will be covered later).

5.3.7. Multiple impl Blocks

Each struct can have multiple impl blocks.

For example, suppose I want to put all the methods and associated functions mentioned in this article into one code sample.
You can write it like this (multiple impl blocks):

#[derive(Debug)]  
struct Rectangle {  
    width: u32,  
    length: u32,  
}  

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

impl Rectangle {  
    fn can_hold(&self, other: &Rectangle) -> bool {  
        self.width > other.width && self.length > other.length  
    }  
}  

impl Rectangle {  
    fn square(size: u32) -> Rectangle {  
        Rectangle{  
            width: size,  
            length: size,  
        }  
    }  
}  

fn main() {  
    let square = Rectangle::square(10);  
    println!("{:?}", square);  
}
Enter fullscreen mode Exit fullscreen mode

You can also write it like this, combining everything into one impl block:

#[derive(Debug)]  
struct Rectangle {  
    width: u32,  
    length: u32,  
}  

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

    fn can_hold(&self, other: &Rectangle) -> bool {  
        self.width > other.width && self.length > other.length  
    }  

    fn square(size: u32) -> Rectangle {  
        Rectangle{  
            width: size,  
            length: size,  
        }  
    }  
}  

fn main() {  
    let square = Rectangle::square(10);  
    println!("{:?}", square);  
}
Enter fullscreen mode Exit fullscreen mode
Read Full Tutorial open_in_new
arrow_back Back to Tutorials