Rust language Bible 23 - Method

Original link: https://course.rs/basic/method.html
 
Welcome to Rust programming college, the best Rust learning community in China

  1. Official website: https://college.rs
  2. QQ group: 1009730433

Method

Students from object-oriented language are certainly familiar with methods. class is full of the concept of methods. In Rust, the concept of methods is not bad, and often appears in pairs with objects:

object.method()

For example, read a file and write it to the buffer. If you use the write method of function read(f,buffer), use the write method of method f.read(buffer) However, unlike the linkage of class and method in other languages, Rust's method is often used together with structure, enumeration and features, which will be introduced in the following chapters.

Definition method

Rust uses impl to define methods, such as the following code:

struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}

impl Circle {
    // new is the correlation function of Circle because its first argument is not self
    // This method is often used to initialize an instance of the current structure
    fn new(x: f64, y: f64, radius: f64) -> Circle {
        Circle {
            x: x,
            y: y,
            radius: radius,
        }
    }

    // Circle method, & self means to borrow the current circle structure
    fn area(&self) -> f64 {
        std::f64::consts::PI * (self.radius * self.radius)
    }
}

We will not explain in detail here, but first establish a general impression of the method definition. The following picture compares the Rust method definition with that of other languages:

It can be seen that all definitions in other languages are in class, but the object definition and method definition of Rust are separated. This separation of data and use will give users great flexibility.

Let's take another example:

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

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

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

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}

This example defines a Rectangle structure and defines an area method on it to calculate the area of the Rectangle.

impl Rectangle {} is expressed as a Rectangle implementation method (impl is the abbreviation for Implementation). This writing indicates that everything in the impl statement block is associated with Rectangle.

The following content is very important. Please read it carefully. In the signature of area, there is a keyword & self, which we haven't seen before. This keyword refers to the & Rectangle type. In other words, self refers to the Rectangle structure. This writing method will make our code much simpler and very easy to understand: which structure we implement the method for, then self refers to the structure itself.

It should be noted that self still has the concept of ownership:

  • self means that the ownership of Rectangle is transferred to this method, which is less used
  • &Self indicates the immutable borrowing of Rectangle by this method
  • &Mut self indicates variable borrowing

In short, the use of self is the same as that of function parameters, which should strictly abide by the ownership rules of Rust.

Returning to the above example, the reason for choosing & self is the same as using & rectangle in the function: we don't want to obtain ownership or change it, but we just want to be able to read the data in the structure. If you want to change the current structure in the method, you need to change the first parameter to & mut self. It is rare for a method to obtain the ownership of an instance by using only self as the first parameter. This method is often used when converting the current object to another object. After the conversion, the previous object will no longer be concerned, and the wrong call to the previous object can be prevented.

In a brief summary, using method instead of function has the following advantages:

  • There is no need to write the type corresponding to self repeatedly in the function signature
  • The code is more organized and cohesive, which is of great benefit to code maintenance and reading

The method name is the same as the structure field name

In Rust, the method name is allowed to be the same as the field name of the structure:

impl Rectangle {
    fn width(&self) -> bool {
        self.width > 0
    }
}

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

    if rect1.width() {
        println!("The rectangle has a nonzero width; it is {}", rect1.width);
    }
}

When we use rect1 When width (), Rust knows that we are calling its method, if rect1. 0 is used Withh is the field that calls it.

Generally speaking, the method has the same name as the field, which is often applicable to the implementation of getter accessors, such as:

pub struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    pub fn new(width: u32, height: u32) -> Self {
        Rectangle { width, height }
    }
    pub fn width(&self) -> u32 {
        return self.width;
    }
}

fn main() {
    let rect1 = Rectangle::new(30, 50);

    println!("{}", rect1.width());
}

In this way, we can set the field of Rectangle as a private property. Just set its new and witth methods to be publicly visible, so the user can create a Rectangle and access rect1 The width () method is used to obtain the width of the Rectangle, because the width field is private when the user accesses rect1 An error will be reported when the withh field is.

->Where's the operator?

In C/C + + language, there are two different operators to call methods: When a method is called directly on an object and a method is called on an object pointer, you need to dereference the pointer first. In other words, if object is a pointer, then object - > something () and (* object) Something () is the same.

Rust does not have an operator equivalent to - > operator; Instead, rust has a feature called auto reference and dereference. Method calls are one of the few places in rust that have this behavior.

He works like this: when using object When something () calls the method, Rust will automatically add &, & mut or * to the object to match the object with the method signature. That is, these codes are equivalent:

# #[derive(Debug,Copy,Clone)]
# struct Point {
#     x: f64,
#     y: f64,
# }
#
# impl Point {
#    fn distance(&self, other: &Point) -> f64 {
#        let x_squared = f64::powi(other.x - self.x, 2);
#        let y_squared = f64::powi(other.y - self.y, 2);
#
#        f64::sqrt(x_squared + y_squared)
#    }
# }
# let p1 = Point { x: 0.0, y: 0.0 };
# let p2 = Point { x: 5.0, y: 6.5 };
p1.distance(&p2);
(&p1).distance(&p2);

The first line looks much more concise. This automatic reference behavior is effective because the method has an explicit receiver, the type of self. Given the receiver and method name, Rust can explicitly calculate whether the method is just read (& self), modify (& mut self), or acquire ownership (self). In fact, Rust's implicit borrowing of method recipients makes ownership more friendly in practice.

Method with multiple parameters

Methods, like functions, can use multiple parameters:

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

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

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };
    let rect2 = Rectangle { width: 10, height: 40 };
    let rect3 = Rectangle { width: 60, height: 45 };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

correlation function

Now you can think about a question, if you define a constructor method for a structure? That is, accept several parameters, and then construct and return an instance of the structure. In fact, the answer is given in the code snippet at the beginning. It's very simple. Don't use self.

This function defined in impl without self is called correlation function: because it has no self and cannot be used in the form of f.read(), it is a function rather than a method. In impl, it is closely related to the structure, so it is called correlation function.

In the previous code, we have used correlation functions many times, such as String::from, to create a dynamic string.

# #[derive(Debug)]
# struct Rectangle {
#     width: u32,
#     height: u32,
# }
#
impl Rectangle {
    fn new(w: u32, h: u32) -> Rectangle {
        Rectangle { width: w, height: h }
    }
}

There is a common name rule in Rust, which uses new as the name of the constructor. For design considerations, Rust specifically does not use new as the keyword

It cannot be used because it is a function To call, we need to call with:: for example, let sq = Rectangle::new(3,3);. This method is located in the namespace of the structure::: syntax is used to associate the namespace created by functions and modules.

Multiple impl definitions

Rust allows us to define multiple impl blocks for a structure in order to provide more flexibility and code organization. For example, when there are many methods, relevant methods can be organized in the same impl block, so multiple impl blocks can be formed to achieve the same goal:

# #[derive(Debug)]
# struct Rectangle {
#     width: u32,
#     height: u32,
# }
#
impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

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

Of course, for this example, we don't need to use two impl blocks. It's just for demonstration convenience.

Implement methods for enumerations

Enum types are powerful not only because they are easy to use and can be used Identity type Also, we can implement methods for enumerations like structs:

#![allow(unused)]
fn main() {
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

impl Message {
    fn call(&self) {
        // The method body is defined here
    }
}

let m = Message::Write(String::from("hello"));
m.call();
}

In addition to structs and enumerations, we can also implement methods for traits, which will be explained in the next chapter. Before that, let's take a look at generics.

Keywords: Back-end Rust

Added by justineaguas on Fri, 10 Dec 2021 17:29:15 +0200