简介
欢迎来到方法语法。本实验是《Rust 程序设计语言》的一部分。你可以在 LabEx 中练习 Rust 技能。
在本实验中,方法使用 fn
关键字和一个名称进行声明,可以有参数和返回值,并且在结构体的上下文中定义,第一个参数始终为 self
,用于表示被调用的结构体实例。
This tutorial is from open-source community. Access the source code
💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版
欢迎来到方法语法。本实验是《Rust 程序设计语言》的一部分。你可以在 LabEx 中练习 Rust 技能。
在本实验中,方法使用 fn
关键字和一个名称进行声明,可以有参数和返回值,并且在结构体的上下文中定义,第一个参数始终为 self
,用于表示被调用的结构体实例。
方法 与函数类似:我们使用 fn
关键字和一个名称来声明它们,它们可以有参数和返回值,并且包含一些在从其他地方调用该方法时运行的代码。与函数不同的是,方法是在结构体(或枚举或 trait 对象,我们将分别在第 6 章和第 17 章中介绍)的上下文中定义的,并且它们的第一个参数始终是 self
,它表示正在调用该方法的结构体实例。
让我们把那个以 Rectangle
实例作为参数的 area
函数进行修改,改为在 Rectangle
结构体上定义一个 area
方法,如清单 5-13 所示。
文件名:src/main.rs
#[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()
);
}
清单 5-13:在 Rectangle
结构体上定义 area
方法
为了在 Rectangle
的上下文中定义函数,我们为 Rectangle
开始一个 impl
(实现)块[1]。这个 impl
块中的所有内容都将与 Rectangle
类型相关联。然后我们把 area
函数移到 impl
花括号内[2],并在签名以及函数体中的各处将第一个(在这种情况下也是唯一的)参数改为 self
。在 main
函数中,我们之前调用 area
函数并传递 rect1
作为参数,现在我们可以使用方法语法来调用 Rectangle
实例上的 area
方法[3]。方法语法紧跟在实例之后:我们添加一个点,后面跟着方法名、括号以及任何参数。
在 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
的方法:
文件名: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!(
"The rectangle has a nonzero width; it is {}",
rect1.width
);
}
}
在这里,如果实例的 width
字段中的值大于 0
,我们选择让 width
方法返回 true
,如果值为 0
则返回 false
:我们可以在同名的方法中出于任何目的使用字段。在 main
函数中,当我们在 rect1.width
后面加上括号时,Rust 知道我们指的是 width
方法。当我们不使用括号时,Rust 知道我们指的是 width
字段。
通常,但不总是,当我们给方法取与字段相同的名字时,我们希望它只返回字段中的值,不做其他事情。像这样的方法被称为获取器,Rust 不像其他一些语言那样为结构体字段自动实现它们。获取器很有用,因为你可以将字段设为私有,但方法设为公共,从而作为类型公共 API 的一部分实现对该字段的只读访问。我们将在第 7 章讨论什么是公共和私有,以及如何将字段或方法指定为公共或私有。
->
运算符在哪里?在 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
实例,并在第二个 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!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
清单 5-14:使用尚未编写的 can_hold
方法
预期输出如下,因为 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
的宽度和高度是否分别大于另一个 Rectangle
的宽度和高度。让我们将新的 can_hold
方法添加到清单 5-13 的 impl
块中,如清单 5-15 所示。
文件名:src/main.rs
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
}
}
清单 5-15:在 Rectangle
上实现接受另一个 Rectangle
实例作为参数的 can_hold
方法
当我们使用清单 5-14 中的 main
函数运行此代码时,我们将得到期望的输出。方法可以接受多个参数,我们在 self
参数之后将它们添加到签名中,并且这些参数的工作方式与函数中的参数相同。
在 impl
块中定义的所有函数都被称为关联函数,因为它们与 impl
后面命名的类型相关联。我们可以定义没有 self
作为第一个参数(因此不是方法)的关联函数,因为它们不需要类型的实例来操作。我们已经使用过这样一个函数:在 String
类型上定义的 String::from
函数。
不是方法的关联函数通常用于构造函数,这些构造函数将返回结构体的一个新实例。这些函数通常被称为 new
,但 new
不是一个特殊的名称,也不是语言内置的。例如,我们可以选择提供一个名为 square
的关联函数,它将有一个维度参数,并将其用作宽度和高度,这样创建一个正方形的 Rectangle
就更容易了,而不必重复指定相同的值两次:
文件名:src/main.rs
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
返回类型[1]和函数体[2]中的 Self
关键字是 impl
关键字后面出现的类型的别名,在这种情况下是 Rectangle
。
要调用这个关联函数,我们使用结构体名称加上 ::
语法;例如 let sq = Rectangle::square(3);
。这个函数是由结构体命名空间限定的:::
语法既用于关联函数,也用于模块创建的命名空间。我们将在第 7 章讨论模块。
每个结构体允许有多个 impl
块。例如,清单 5-15 等同于清单 5-16 中所示的代码,其中每个方法都在自己的 impl
块中。
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
}
}
清单 5-16:使用多个 impl
块重写清单 5-15
在这里没有理由将这些方法分成多个 impl
块,但这是有效的语法。在第 10 章讨论泛型类型和 trait 时,我们会看到一个多个 impl
块很有用的例子。
恭喜你!你已经完成了「方法语法」实验。你可以在 LabEx 中练习更多实验来提升你的技能。