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 thestructinstance the method belongs to and is called on, similar toselfin Python andthisin 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
}
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
}
}
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
}
}
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());
}
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());
}
Output:
1500
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
implblock, so you do not have to search the entire codebase for behaviors related to astruct.
5.3.4. Operators for Method Calls
In C/C++, there are two operators for calling methods:
-
->: The format isobject->something(). Use this to call methods on the object pointed to by a pointer (that is, whenobjectis a pointer). -
.: The format isobject.something(). Use this to call methods on the object itself (that is, whenobjectis 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);
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
}
}
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));
}
Output:
true
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,
}
}
}
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);
}
Output:
Rectangle { width: 10, length: 10 }
:: 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);
}
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);
}
Top comments (0)