Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

5.3 struct的方法(Method)

5.3.1. 什么是方法(Method)

方法和函数类似,也是用 fn 关键字进行声明,方法也有名称、参数和返回值。但方法和函数也有不同之处:

  • 方法在 struct(或枚举或 trait 对象)的上下文中定义。
  • 方法的第一个参数总是 self,表示方法所属(被调用)的 struct 实例,类似于 Python 中的 self 和 JavaScript 中的 this

5.3.2. 方法的实际应用

接下来还是看例子,以上一篇文章 5.2. struct使用例(加打印调试信息) 的代码为例:

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
}

area 这个函数的作用是计算面积,但它很特别:它只适用于矩形,而不适用于其他形状或其他类型。如果后面要加上计算其他图形面积的函数,那么 area 这个名字就会变得含糊。如果改名成 rectangle_area 又太麻烦,因为 main 里所有调用这个函数的地方也都要改。

所以如果能把存储矩形长宽的 Rectangle 结构体,和只能计算矩形面积的 area 函数结合到一起,就是最好的。

对于这种需求,Rust 提供了“实现”(implementation),其关键字是 impl。后边跟着 struct 名,加上一对 {},在里面像定义普通函数一样定义方法就行。

对于这个例子,struct 名就是 Rectangle,把定义 area 函数的代码剪贴到 {} 内即可:

#![allow(unused)]
fn main() {
impl Rectangle {
    fn area(dim:&Rectangle) -> u32 {
        dim.width * dim.length
    }
}
}

但注意这里的代码还不是方法,因为方法的第一个参数必须是 self。现在的代码叫关联函数,下文会讲。

这么写是没有问题的,但还可以进一步简化。上文中说到了方法的第一个参数总是 self,所以这里也可以改一下:

#![allow(unused)]
fn main() {
impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.length
    }
}
}

你当前写的这个方法绑定在谁上,self 指的就是谁。这个代码中 area 函数被绑定在 Rectangle 上,所以 self 就指的是 Rectanglearea 的参数不用拿走所有权,所以在 self 前面加上 & 表示引用。

当然这么改之后,main 函数里的函数调用也要改——从函数调用改到方法调用:实例.方法名(参数)

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

rectangle.area() 的括号中不写东西,是因为 area 方法在定义时只使用了 &self 作为参数,表示这个方法借用了 self(即 rectangle 实例)的不可变引用。在调用 area 时,你不需要显式地传递这个实例,因为方法调用已经隐式地知道 selfrectangle

整体代码如下:

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());
}

输出:

1500

5.3.3. 如何定义方法

在上面的实际应用中已经写过一遍了,所以这里就只做总结:

  • impl 里定义方法
  • 方法的第一个参数可以是 self&self&mut self。可以获得所有权、不可变引用或可变引用,这点和其他参数一样。
  • 方法可以帮助更好地组织代码,因为可以把某个类型的方法都放在同一个 impl 块里面,这样就不必在整个代码库里搜索与某个 struct 相关的行为了。

5.3.4. 方法调用的运算符

在 C/C++ 中,调用方法有两种运算符:

  • ->:其格式为 object->something()。调用指针指向的对象上的方法就使用这一种(也就是 object 为指针时)。
  • .:其格式为 object.something()。调用对象本身上的方法就使用这种(也就是 object 不为指针,是个对象时)。

object->something() 实际上是语法糖,它等同于 (*object).something()* 表示解引用。两者的流程都是先解引用,得到对象,再在对象上调用方法。

Rust 提供了自动引用/解引用的特性。也就是说,在调用方法时,Rust 会根据情况自动添加 &&mut*,以便 object 可以匹配方法的签名。这点和 Go 语言一样。

举个例子,下面这两行代码效果相同:

#![allow(unused)]
fn main() {
point1.distance(&point2);
(&point1).distance(&point2);
}

Rust 会根据情况自动在 point1 前加上 &

5.3.5. 方法的参数

方法除了 self 也可以带其他参数,一个或多个都可以。

举个例子,在 5.3.2 的代码基础上加一个判断矩形是否能容纳下另一个矩形的功能(不考虑斜着放,也不考虑矩形的长比宽长的情况):

#![allow(unused)]
fn main() {
impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.length > other.length
    }
}
}

逻辑非常好想:只要矩形的宽和长都比另一个大就行。

然后再在 main 函数里声明几个 Rectangle 实例,输出比较结果看看有没有问题就行。以下是完整代码:

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));
}

输出:

true

5.3.6. 关联函数

可以在 impl 块里定义不把 self 作为第一个参数的函数,叫关联函数(不是方法)。它不是在实例上调用的,但它与这个类型有关联。例如:String::from() 就是 String 这个类型上叫做 from 的关联函数。

关联函数通常用于构造器,也就是用来创建关联类型的一个实例。

比如说,在 5.3.2 的代码基础上加一个构建正方形的构造器(正方形也是特殊的矩形):

#![allow(unused)]
fn main() {
impl Rectangle {
    fn square(size: u32) -> Rectangle {
        Rectangle{
            width: size,
            length: size,
        }
    }
}
}

参数只需要一个,因为构造正方形只需要一个边长。

main 函数里调用一下这个关联函数试试,其格式为 类型名::函数名(参数)。以下是完整代码:

#[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);
}

输出:

Rectangle { width: 10, length: 10 }

:: 不仅可以用于关联函数,也可以用于模块创建命名空间(以后会讲)。

5.3.7. 多个impl

每个 struct 允许拥有多个 impl 块。

比如我要把这篇文章里写过的所有方法和关联函数都写到一个代码示例里。 可以这么写(多个 impl 块):

#[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);
}

也可以这么写,合在一个 impl 块里:

#[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);
}