方法语法
方法类似于函数:我们使用 fn
关键字和一个名称来声明它们,它们可以有参数和返回值,并且包含一些在方法从其他地方被调用时运行的代码。与函数不同,方法是在结构体(或枚举或 trait 对象,我们在 第 6 章和 第 18 章分别介绍)的上下文中定义的,并且它们的第一个参数始终是 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
(实现)块。此 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
的方法
文件名: src/main.rs
#[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
。
通常,但不总是,当我们给方法与字段相同的名称时,我们希望它仅返回字段中的值,而不做其他任何事情。这样的方法称为 getter,Rust 不像某些其他语言那样为结构体字段自动实现它们。Getter 很有用,因为您可以使字段私有但方法公开,从而允许对该字段进行只读访问,作为类型公共 API 的一部分。我们将在 第 7 章
中讨论什么是公共和私有,以及如何将字段或方法指定为公共或私有 .带有更多参数的方法
让我们通过在 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
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } 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)); }
列表 5-15: 在 Rectangle
上实现 can_hold
方法,该方法将另一个 Rectangle
实例作为参数
当我们使用列表 5-14 中的 main
函数运行此代码时,我们将获得所需的输出。方法可以接受多个参数,我们在 self
参数之后将这些参数添加到签名中,并且这些参数的工作方式与函数中的参数完全相同。
关联函数
在 impl
块中定义的所有函数都称为关联函数,因为它们与 impl
后命名的类型关联。我们可以将关联函数定义为没有 self
作为其第一个参数的函数(因此不是方法),因为它们不需要类型的实例即可工作。我们已经使用过一个这样的函数:在 String
类型上定义的 String::from
函数。
不是方法的关联函数通常用于构造函数,构造函数将返回结构体的新实例。这些通常称为 new
,但 new
不是特殊名称,也没有内置到语言中。例如,我们可以选择提供一个名为 square
的关联函数,它将具有一个维度参数,并将其用作宽度和高度,从而更容易创建一个正方形 Rectangle
,而不是必须指定两次相同的值
文件名: src/main.rs
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn square(size: u32) -> Self { Self { width: size, height: size, } } } fn main() { let sq = Rectangle::square(3); }
返回类型和函数主体中的 Self
关键字是出现在 impl
关键字后的类型的别名,在本例中为 Rectangle
。
要调用此关联函数,我们使用带有结构体名称的 ::
语法;let sq = Rectangle::square(3);
是一个示例。此函数由结构体命名空间:::
语法用于关联函数和模块创建的命名空间。我们将在 第 7 章
多个 impl
块
每个结构体都允许有多个 impl
块。例如,列表 5-15 等效于列表 5-16 中显示的代码,后者将每个方法放在其自己的 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)); }
列表 5-16: 使用多个 impl
块重写列表 5-15
这里没有理由将这些方法分隔到多个 impl
块中,但这是一种有效的语法。我们将在第 10 章中看到多个 impl
块有用的情况,我们在其中讨论泛型和 trait。
方法调用是函数调用的语法糖
使用我们到目前为止讨论的概念,我们现在可以看到方法调用是如何成为函数调用的语法糖的。例如,假设我们有一个带有 area
方法和 set_width
方法的 rectangle 结构体
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn set_width(&mut self, width: u32) {
self.width = width;
}
}
假设我们有一个矩形 r
。那么方法调用 r.area()
和 r.set_width(2)
等效于此
struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } fn set_width(&mut self, width: u32) { self.width = width; } } fn main() { let mut r = Rectangle { width: 1, height: 2 }; let area1 = r.area(); let area2 = Rectangle::area(&r); assert_eq!(area1, area2); r.set_width(2); Rectangle::set_width(&mut r, 2); }
方法调用 r.area()
变为 Rectangle::area(&r)
。函数名称是关联函数 Rectangle::area
。函数参数是 &self
参数。Rust 会自动插入借用运算符 &
。
注意:如果您熟悉 C 或 C++,您会习惯于两种不同的方法调用语法:
r.area()
和r->area()
。Rust 没有等效于箭头运算符->
的运算符。当您使用点运算符时,Rust 将自动引用和解引用方法接收器。
方法调用 r.set_width(2)
类似地变为 Rectangle::set_width(&mut r, 2)
。此方法期望 &mut self
,因此第一个参数是可变借用 &mut r
。第二个参数完全相同,数字 2。
正如我们在第 4.3 章 “解引用指针访问其数据” 中描述的那样,Rust 将插入尽可能多的引用和解引用,以使类型与 self
参数匹配。例如,以下是对盒装矩形的可变引用的 area
的两个等效调用
struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } fn set_width(&mut self, width: u32) { self.width = width; } } fn main() { let r = &mut Box::new(Rectangle { width: 1, height: 2 }); let area1 = r.area(); let area2 = Rectangle::area(&**r); assert_eq!(area1, area2); }
Rust 将添加两个解引用(一个用于可变引用,一个用于 box),然后添加一个不可变借用,因为 area
期望 &Rectangle
。请注意,这也是可变引用“降级”为共享引用的情况,就像我们在 第 4.2 章 中讨论的那样。相反,您将不允许在 &Rectangle
或 &Box<Rectangle>
类型的值上调用 set_width
。
方法与所有权
就像我们在第 4.2 章 “引用与借用” 中讨论的那样,方法必须在具有必要权限的结构体上调用。作为一个运行示例,我们将使用这三个分别接受 &self
、&mut self
和 self
的方法。
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn set_width(&mut self, width: u32) {
self.width = width;
}
fn max(self, other: Rectangle) -> Rectangle {
Rectangle {
width: self.width.max(other.width),
height: self.height.max(other.height),
}
}
}
使用 &self
和 &mut self
进行读取和写入
如果我们使用 let rect = Rectangle { ... }
创建一个拥有的矩形,则 rect
具有 R 和 O 权限。使用这些权限,可以调用 area
和 max
方法
但是,如果我们尝试调用 set_width
,我们将缺少 W 权限
Rust 将拒绝此程序并显示相应的错误
error[E0596]: cannot borrow `rect` as mutable, as it is not declared as mutable
--> test.rs:28:1
|
24 | let rect = Rectangle {
| ---- help: consider changing this to be mutable: `mut rect`
...
28 | rect.set_width(0);
| ^^^^^^^^^^^^^^^^^ cannot borrow as mutable
如果我们尝试在 Rectangle
的不可变引用上调用 set_width
,即使底层矩形是可变的,我们也会收到类似的错误
使用 self
进行移动
调用期望 self
的方法将移动输入结构体(除非该结构体实现了 Copy
)。例如,我们在将其传递给 max
后无法使用 Rectangle
一旦我们调用 rect.max(..)
,我们就会移动 rect
,因此失去了对其的所有权限。尝试编译此程序将给我们以下错误
error[E0382]: borrow of moved value: `rect`
--> test.rs:33:16
|
24 | let rect = Rectangle {
| ---- move occurs because `rect` has type `Rectangle`, which does not implement the `Copy` trait
...
32 | let max_rect = rect.max(other_rect);
| --------------- `rect` moved due to this method call
33 | println!("{}", rect.area());
| ^^^^^^^^^^^ value borrowed here after move
如果我们尝试在引用上调用 self
方法,也会出现类似的情况。例如,假设我们尝试创建一个方法 set_to_max
,该方法将 self
分配给 self.max(..)
的输出
然后我们可以看到,在操作 self.max(..)
中,self
缺少 O 权限。因此,Rust 拒绝此程序并显示以下错误
error[E0507]: cannot move out of `*self` which is behind a mutable reference
--> test.rs:23:17
|
23 | *self = self.max(other);
| ^^^^^----------
| | |
| | `*self` moved due to this method call
| move occurs because `*self` has type `Rectangle`, which does not implement the `Copy` trait
|
这与我们在第 4.3 章 “修复不安全的程序:复制与从集合中移出” 中讨论的错误类型相同。
好的移动和坏的移动
您可能想知道:如果我们从 *self
中移出,有什么关系呢?实际上,对于 Rectangle
的情况,从 *self
中移出实际上是安全的,即使 Rust 不允许您这样做。例如,如果我们模拟一个调用被拒绝的 set_to_max
的程序,您可以看到没有发生不安全的事情
从 *self
中移出是安全的原因是 Rectangle
不拥有任何堆数据。实际上,我们实际上可以通过简单地将 #[derive(Copy, Clone)]
添加到 Rectangle
的定义中来让 Rust 编译 set_to_max
请注意,与以前不同,self.max(other)
不再需要 *self
或 other
上的 O 权限。请记住,self.max(other)
解糖为 Rectangle::max(*self, other)
。如果 Rectangle
是可复制的,则解引用 *self
不需要对 *self
的所有权。
您可能想知道:为什么 Rust 不会自动为 Rectangle
派生 Copy
?Rust 不会自动派生 Copy
是为了跨 API 更改保持稳定性。想象一下,Rectangle
类型的作者决定添加一个 name: String
字段。那么,所有依赖于 Rectangle
为 Copy
的客户端代码都会突然被编译器拒绝。为了避免这个问题,API 作者必须显式添加 #[derive(Copy)]
以指示他们期望他们的结构体始终为 Copy
。
为了更好地理解这个问题,让我们运行一个模拟。假设我们向 Rectangle
添加了 name: String
。如果 Rust 允许 set_to_max
编译,会发生什么?
在此程序中,我们使用两个矩形 r1
和 r2
调用 set_to_max
。self
是对 r1
的可变引用,other
是 r2
的移动。在调用 self.max(other)
后,max
方法消耗了两个矩形的所有权。当 max
返回时,Rust 会在堆中释放两个字符串 “r1” 和 “r2”。请注意问题:在位置 L2,*self
应该是可读写的。但是,(*self).name
(实际上是 r1.name
)已被释放。
因此,当我们执行 *self = max
时,我们会遇到未定义的行为。当我们覆盖 *self
时,Rust 会隐式地丢弃先前在 *self
中的数据。为了使该行为显式化,我们添加了 drop(*self)
。在调用 drop(*self)
后,Rust 会尝试第二次释放 (*self).name
。该操作是双重释放,这是未定义的行为。
所以请记住:当您看到类似 “cannot move out of *self
” 的错误时,通常是因为您尝试在类似 &self
或 &mut self
的引用上调用 self
方法。Rust 正在保护您免受双重释放。
总结
结构体使您可以创建对您的领域有意义的自定义类型。通过使用结构体,您可以使关联的数据片段彼此连接,并命名每个片段以使您的代码清晰。在 impl
块中,您可以定义与您的类型关联的函数,而方法是一种关联函数,可让您指定结构体实例的行为。
但是结构体不是您可以创建自定义类型的唯一方法:让我们转向 Rust 的枚举功能,为您的工具箱添加另一个工具。