方法语法

Method Syntax

方法 与函数类似:我们用 fn 关键字和一个名字,来声明方法,方法可以有参数和返回值,同时他们还包含一些代码,这些代码会在从其他地方调用该方法时运行。与函数不同的是,方法是在某个结构体(或某个枚举或特质对象,我们将在 第 6 章第 17 章 分别介绍)的上下文中定义的,而且方法的第一个参数,总是表示该方法被调用所在的结构体实例本身的 self

定义出方法

Defining Methods

如下清单 5-13 所示,我们来修改以 Rectangle 实例为参数的那个 area 函数,转而构造出一个定义在 Rectangle 结构上的 area 方法。

#[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! ("该矩形的面积为 {} 平方像素。",
        rect1.area()
    );
}

清单 5-13:在 Rectangle 结构体上定义一个 area 方法

为了在 Rectangle 上下文中定义这个函数,我们为 Rectangle 创建了一个 impl(implementation,实现)代码块。该 impl 代码块中的所有内容,都将与 Rectangle 这个类型相关联。然后,我们将那个 area 函数,移入 impl 的花括号中,并把函数签名中的首个(且本示例中唯一的)参数修改为 self, 同时修改函数体中的各处。在 main 中,在我们曾调用过 area 函数并将 rect1 作为参数传递的地方,我们便可以使用 方法语法,method syntax,在咱们的 Rectangle 实例上,调用 area 方法。方法语法位于某个实例之后:我们要添加一个后跟方法名称、圆括号和任何参数的一个点。

area 的签名中,我们使用了 &self 而不是 rectangle: &Rectangle&self 实际上是 self:&Self 的缩写。在 impl 代码块中,Self 这个类型,是 impl 代码块,所针对的那个类型的别名。方法必须将名为 self,类型为 Self 的参数,作为其第一个参数,因此 Rust 允许咱们,在第一个参数处,将其缩写为仅 self 这个名字。请注意,就像在 rectangle: &Rectangle 中一样,我们仍然需要在 self 这个简写前面,使用 & 来表明该方法借用了 Self 这个实例。方法可以取得 self 的所有权,也可以不可变地借用 self(就像我们在这里所做的),还可以可变地借用 self(就像借用其他参数一样)。

译注&self - 不可变借用;&mut self 可变借用;self - 取得所有权,发生所有权转移,self 所指向的内存堆上值,原先那个在栈上的变量将失效。

我们在这里选择 &self 的原因,与我们在函数那个版本中使用 &Rectangle 的原因相同:我们不打算取得所有权,我们只想读取该结构体中的数据,而不是向其写数据。如果我们打算修改调用方法的实例(作为该方法的一部分),我们可以使用 &mut self 作为第一个参数。只使用 self 作为第一个参数,来取得实例所有权的方法并不多见;这种方法通常用于该方法会将 self 转换成其他东西,且咱们想要防止调用者,在这种转换后继续使用原始实例的时候。

除了提供方法语法和不必在每个方法的签名中,重复 self 的类型外,使用方法而不是函数的主要原因,是为了组织。我们把所有能用类型实例做的事情,都放在一个 impl 块中,而不是让我们代码的未来用户,在我们提供的库中不同地方,检索 Rectangle 的功能。

请注意,我们可以选择将某个方法,命名为与结构体的某个字段同名。例如,我们可以为 Rectangle 定义一个名为 width 的方法:

文件名:src/main.rs

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

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

    if rect1.width() {
        println! ("该矩形的宽度不为零;其为 {}", rect1.width);
    }
}

在这里,我们选择将这个 width 方法,构造为在实例的 width 字段中值大于 0 时,返回 true;如果该值为 0,则返回 false:我们可以在与某个字段的同名方法中,使用这个字段来达到任何目的。在 main 中,当我们在 rect1.width 后加上括号时,Rust 就知道我们指的是 width 这个方法。当我们不使用括号时,Rust 知道我们指的是 width 字段。

通常(但不总是),当我把某个方法,命名为与一个字段同名时,我们就希望他只返回字段中的值,而不做其他任何事情。这样的方法称为 获取器,getter,而 Rust 并未像其他语言那样,自动为结构的字段实现获取器。获取器之所以有用,是因为我们可以将该字段,构造为私有,而将该方法构造为公开,从而将对该字段的只读访问,实现为该类型公开 API 的一部分。我们将在 第 7 章 讨论什么是公开,public 和私有,private,以及如何将字段或方法,指定为公开或私有。

-> 操作符在哪里?

在 C 和 C++ 中,有两种不同的操作符用于调用方法:如果是直接调用对象上的方法,咱们会使用 .;如果是调用到对象的某个指针上的方法,并且需要首先解引用该指针时,则要使用 ->。换句话说,如果 object 是个指针,则 object->something() 类似于 (*object).something()

Rust 没有与 -> 等价的运算符;相反,Rust 有一项称为 自动引用和解引用,automatic referencing and dereferencing 的特性。在 Rust 中,调用方法是少数几个具有这种行为的地方之一。

其工作原理如下:当咱们以 object.something() 调用某个方法时,Rust 会自动加入 &&mut*,以便 object 与该方法的签名相匹配。换句话说,下面两个方法是相同的:

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

第一种看起来更简洁。这种自动引用行为之所以有效,是因为方法有明确的接收者 -- 类型 self。有了方法的接收者和名字,Rust 就能明确确定,该方法是在读取 (&self)、改变 (&mut self) 还是消费 (self)。Rust 将方法的接收者,构造为隐式借用,这一事实,是在实践中,令到所有权符合人机工程学的重要部分。

带有更多参数的方法

Methods with More Parameters

我们来通过在 Rectangle 结构体上,实现第二个方法,来练习运用方法。这一次,我们希望 Rectangle 的实例,取另一 Rectangle 实例,在第二个 Rectangle 可以完全容纳在 self(第一个 Rectangle)中时,则返回 true;否则,返回 false。也就是说,只要我们已定义出 can_hold 方法,我们就可以编写下面清单 5-14 中所示的程序。

文件名:src/main.rs

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

    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };

    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println! ("rect1 可以容纳 rect2 吗?{}", rect1.can_hold(&rect2));
    println! ("rect1 可以容纳 rect3 吗?{}", rect1.can_hold(&rect3));
}

清单 5-14:使用尚未编写的 can_hold 方法

由于 rect2 两个边都小于 rect1 的两个边,而 rect3 则宽于 rect1,因此预期输出结果如下:

rect1 可以容纳 rect2 吗?true
rect1 可以容纳 rect3 吗?false

我们知道咱们是要定义某个方法,因此其将位于那个 impl Rectangle 代码块中。方法的名称,将是 can_hold,同时他将取另一 Rectangle 的不可变借用作为参数。通过查看调用该方法的代码,我们可以判断出该参数的类型:rect1.can_hold(&rect2) 传入了 &rect2,其为对 Rectangle 实例 rect2 的不可变借用。这是有道理的,因为我们只需要读取 rect2(而不是写入,那意味着我们需要一个可变借用),而且我们希望 main 保留对 rect2 的所有权,这样我们就可以在调用 can_hold 方法后,再次使用他。can_hold 的返回值,将是个布尔值,其实现将检查 self 宽度和高度,是否分别大于另一 Rectangle 的宽度和高度。咱们来将这个新的 can_hold 方法,添加到清单 5-13 中的 impl 代码块中,如下清单 5-15 所示。

文件名:src/main.rs

#![allow(unused)]
fn main() {
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) ||
            (self.width > other.height && self.height > other.width)
    }
}
}

清单 5-15:在 Rectangle 上实现这个会取另一 Rectangle 实例作为参数的 can_hold 方法

当我们使用清单 5-14 中的 main 函数,运行这段代码时,就会得到我们想要的输出。方法可以取我们在 self 参数后,添加到签名的多个参数,而这些参数的作用,就跟函数中的参数一样。

译注:实际上,这个 can_hold 的实现有错误,因为其没有考虑到矩形倒转后可以容纳的情况,改进后的 can_hold 如下。

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

}

关联函数

associated functions

impl 代码块中定义的所有函数,都被称为 关联函数,associated functions,因为他们与 impl 后命名的那个类型相关联。我们可以定义不以 self 作为第一个参数的关联函数(而因此就不属于方法),因为他们不需要处理该类型的某个实例。我们已经使用过一个这样的函数:定义在 String 类型上的 String::from 函数。

非方法的关联函数,通常用于返回结构体新实例的构造函数。这些函数通常被称为 new,但 new 并不是个特殊的名字,也没有内置在语言中。例如,我们可以选择提供一个名为 square 的关联函数,该函数只有一个边长参数,并将其同时用作宽度和高度,这样就可以更轻松地创建一个正方形的 Rectangle,而不必两次指定同一个值两次:

文件名:src/main.rs

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

返回类型和函数体中的两个 Self 关键字,是出现于 impl 关键字后那个类型的别名,在本例中即 Rectangle

要调用这个关联函数,我们要使用带有结构体名字的 :: 语法;let sq = Rectangle::square(3); 就是个例子。该函数的命名空间为该结构体::: 这种语法,既用于关联函数,也用于由模块创建出的命名空间。我们将在 第 7 章 讨论模块。

多个 impl 代码块

Multiple impl Blocks

每个结构体,都可以有多个 impl 块。例如,清单 5-15 就相当于下面清单 5-16 中的代码,其中每个方法都有自己的 impl 块。

#![allow(unused)]
fn main() {
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) ||
            (self.width > other.height && self.height > other.width)
    }
}
}

清单 5-16:使用多个 impl 代码块重写清单 5-15

这里没有理由,将这些方法分隔成多个 impl 块,但这是有效的语法。在第 10 章讨论泛型和特质时,我们将看到多个 impl 代码块的用处。

本章小结

结构体可让咱们创建出,对咱们领域有意义的自定义类型。通过使用结构体,咱们可以将相关的数据片段,相互连接起来,并为每个片段命名,使咱们的代码清晰明了。在 impl 代码块中,咱们可以定义出,与咱们类型相关联的函数,而方法就是一种,可以让咱们指定出,结构体实例所具有行为的关联函数。

但是,结构体并不是咱们可以创建自定义类型的唯一方法:咱们来使用 Rust 的枚举特性,为咱们的工具箱添加另一种工具。

Last change: 2023-12-15, commit: 9e7b57f