前言
在这一章,我们将一起学习下方法语法,方法类似于函数:我们使用 fn
关键字和名称声明它们,它们可以有参数和返回值,并且它们包含一些代码,当从其他地方调用方法时,这些代码会运行。与函数不同,方法是在结构体(或枚举或 trait 对象,我们将会在后续来一起学习)的上下文中定义的,它们的第一个参数始终是 self
,它表示调用该方法的结构体的实例。
内容
定义方法
让我们改变一下以 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!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}
为了在 Rectangle
的上下文中定义函数,我们为 Rectangle
启动一个 impl
(实现) 块。此 impl
块中的所有内容都将与 Rectangle
类型相关联。然后我们将 area
函数移动到 impl
大括号内,并将第一个参数更改为签名中的 self
。然后在 main
中,我们调用 area
函数并将 rect1
作为参数传递,现在我们可以改用方法语法
来调用 Rectangle
实例上的 area
方法。在方法语法在实例之后,我们就可以直接调用了。
在 area
的签名中,我们使用 &self
而不是 rectangle: &Rectangle
。&self
实际上是 self: &Self
的缩写。在 impl
块中,类型 Self
是 impl
块所针对的类型的别名。方法的第一个参数必须有一个名为 self
的参数,类型为 Self
,因此 Rust 允许你在第一个参数位置只使用名称 self
来缩写它。请注意,我们仍然需要在 self
简写前面使用 &
来表示此方法借用了 Self
实例,就像我们在 rectangle: &Rectangle
中所做的那样。方法可以选择获得 self
的所有权,或者像我们这里一样不可变地借用 self
,或者可变地借用 self
,就跟其他参数一样。
我们在这里选择 &self
的原因与在函数版本中使用 &Rectangle
的原因相同:我们不想获得所有权,我们只想读取结构体中的数据,而不是写入它。如果我们想更改调用该方法的实例作为方法功能的一部分,我们将使用 &mut self
作为第一个参数。很少有方法仅使用 self
作为第一个参数来获取实例的所有权;这种情况通常用在当方法将 self
转换成别的实例的时候,我们想要阻止调用方在转换后使用原始实例。
使用方法而不是函数的主要原因,除了可使用方法语法和不需要在每个函数签名中重复 self
的类型之外,其主要好处在于组织性。我们将某个类型实例能做的所有事情都一起放入 impl
块中,而不是让将来的用户在我们的库中到处寻找 Rectangle
的功能。
请注意,我们可以选择将方法的名称与结构中的一个字段相同。例如,我们可以在 Rectangle
上定义一个名为 width
方法:
#[allow(dead_code)]
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
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);
}
}
在这里,我们选择让 width
方法如果在实例上的 width
字段值大于 0
时返回 true
,如果值为 0
,则返回 false
:我们可以将同名方法中的字段用于任何目的。在 main
中,当我们在 rect1.width
后面带括号的,Rust 知道我们指的是方法 width
。当我们不使用括号时,Rust 知道我们指的是字段width
。
通常,但并非总是如此,当我们为方法指定与字段相同的名称时,我们希望它只返回字段中的值,而不执行任何其他操作。像这样的方法称为 getters,而 Rust 不会像其他一些语言那样自动为结构字段实现它们。Getters 很有用,因为您可以将字段设为私有的,但将方法设为公共的,从而启用对该字段的只读访问,作为类型的公共API的一部分。
->运算符到哪去了?
在 C 和 C++ 中,使用两种不同的运算符来调用方法:如果使用 .(如果直接调用对象上的方法),则使用 ->(如果对指向对象的指针调用方法,并且需要先取消引用指针)。换句话说,如果 object 是一个指针,则 object->something() 类似于 (*object).something()。
Rust 没有等价于 -> 运算符;相反,Rust 有一个叫做自动引用和取消引用的功能。调用方法是 Rust 中为数不多的具有此行为的地方之一。
它的工作原理是这样的:当你使用 object.something() 调用一个方法时,Rust 会自动添加 &、&mut 或 *,以便 object 匹配方法的签名。换句话说,以下内容是相同的:
p1.distance(&p2);
(&p1).distance(&p2);
第一个看起来更干净。这种自动引用行为之所以有效,是因为方法有一个明确的接收器 — self 类型。给定方法的接收者和名称,Rust 可以明确地确定该方法是读取 (&self)、突变 (&mut self) 还是消耗 (self)。Rust 使方法接收者隐式借用这一事实是在实践中使所有权符合人体工程学的重要组成部分。
具有更多参数的方法
让我们通过在 Rectangle
结构体上实现第二个方法来练习使用方法。这一次,我们希望 Rectangle
的实例采用 Rectangle
的另一个实例,如果 self
能完全包含第二个长方形则返回 true
;否则返回 false
。也就是说,一旦我们定义了 can_hold
方法,我们希望能够编写如下代码:
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));
}
预期输出将如下所示,因为 rect2
的两个维度都小于 rect1
的维度,但 rect3
比 rect1
宽:
Can rect1 hold rect2? true
Can rect1 hold rect3? false
我们知道我们想要定义一个方法,所以它将在 impl Rectangle
块中。方法名称将为 can_hold
,并且它将不可变地借用另一个 Rectangle
作为参数。我们可以通过查看调用该方法的代码来判断参数的类型:rect1.can_hold(&rect2)
传入 &rect2
,这是对 rect2
(Rectangle
的实例)的不可变借用。这是可以理解的,因为我们只需要读取 rect2
(而不是写入,这意味着我们需要一个可变的借用),并且我们希望 main
保留 rect2
的所有权,以便我们可以在调用 can_hold
方法后再次使用它。can_hold
的返回值将是一个布尔值,实现将分别检查 self
的 width
和 height
是否大于另一个 Rectangle
的 width
和 height
。让我们将新的 can_hold
方法添加到 impl
块中,如下 所示:
#[allow(dead_code)]
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
后增加多个参数,而且这些参数就像函数中的参数一样工作。
关联函数
impl
块中定义的所有函数都称为关联函数,因为它们与以 impl
命名的类型相关联。我们可以定义不将 self
作为其第一个参数的关联函数(因此不是方法),因为它们不需要该类型的实例来使用。我们已经使用了一个函数,如下所示:在 String
类型上定义的 String::from
函数。
不是方法的关联函数通常用于将返回结构的新实例的构造函数。这些通常称为 new
,但 new
不是一个特殊名称,也不内置于语言中。例如,我们可以选择提供一个名为 square
的关联函数,该函数将具有一个 dimension 参数,并将其用作 width 和 height,从而更容易创建方形 Rectangle
,而不必两次指定相同的值:
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
返回类型和函数正文中的 Self
关键字是出现在 impl
关键字(在本例中为 Rectangle
)之后的类型的别名。
使用结构体名和 ::
语法来调用这个关联函数:比如 let sq = Rectangle::square(3);
。这个方法位于结构体的命名空间中:::
语法用于关联函数和模块创建的命名空间。
多个impl块
每个结构体都允许有多个 impl
块。如下代码所示,其中每个方法都在自己的 impl
块中。
#[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
}
}
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));
}
没有理由在这里将这些方法分成多个 impl
块,但这是有效的语法。
总结
结构体让你可以创建出在你的领域中有意义的自定义类型。通过结构体,我们可以将相关联的数据片段联系起来并命名它们,这样可以使得代码更加清晰。在 impl
块中,你可以定义与你的类型相关联的函数,而方法是一种相关联的函数,允许您指定结构体的实例具有的行为。
但是结构体并不是创建自定义类型的唯一方式:让我们转向 Rust 的 enum 功能,将另一个工具添加到你的工具箱中。