所有权
如果你按照本课程目前所学的内容解决了上一个练习,你的访问器方法可能看起来像这样:
#![allow(unused)] fn main() { impl Ticket { pub fn title(self) -> String { self.title } pub fn description(self) -> String { self.description } pub fn status(self) -> String { self.status } } }
这些方法可以编译通过,并且足以让测试通过,但在实际场景中,它们不会让你走得太远。考虑这段代码:
#![allow(unused)] fn main() { if ticket.status() == "待办" { // 尽管我们还没讲到 `println!` 宏, // 但目前只需知道它会将(模板化的)消息打印到控制台 println!("你的下一个任务是: {}", ticket.title()); } }
如果你尝试编译它,你会得到一个错误:
错误[E0382]: 使用了已移动的值: `ticket`
--> src/main.rs:30:43
|
25 | let ticket = Ticket::new(/* */);
| ------ `ticket` 因为此处类型为 `Ticket` 而被移动,
| 此类型未实现 `Copy` 特征
26 | if ticket.status() == "待办" {
| -------- `ticket` 因为此方法调用而被移动
...
30 | println!("你的下一个任务是: {}", ticket.title());
| ^^^^^^ 在移动后此处再次使用了值
|
注意: `Ticket::status` 接收者 `self` 采用所有权,这导致 `ticket` 被移动
--> src/main.rs:12:23
|
12 | pub fn status(self) -> String {
| ^^^^
恭喜,这是你遇到的第一个借用检查器错误!
Rust所有权系统的优点
Rust的所有权系统旨在确保:
- 数据在被读取时从不被修改
- 数据在被修改时从不被读取
- 数据被销毁后不再被访问
这些约束由借用检查器强制执行,它是Rust编译器的一个子系统,经常成为Rust社区中笑话和梗的主题。
所有权是Rust中的一个关键概念,也是使该语言独特的原因。所有权使得Rust能够在不牺牲性能的情况下提供内存安全性。对于Rust来说,以下所有内容同时都是真实的:
- 没有运行时垃圾收集器
- 作为开发者,你很少需要直接管理内存
- 你无法造成悬挂指针、重复释放以及其他与内存相关的错误
像Python、JavaScript和Java这样的语言给你2.和3.,但不提供1.。像C或C++这样的语言给你1.,但不提供2.和3.。
根据你的背景,3.听起来可能有点神秘:什么是“悬挂指针”?什么是“重复释放”?为什么它们危险?别担心:我们将在课程的其余部分更详细地讨论这些概念。
不过,目前,让我们先专注于学习如何在Rust的所有权系统下工作。
所有者
在Rust中,每个值都有一个所有者,在编译时静态确定。在任何给定时间,每个值只有一个所有者。
移动语义
所有权可以转移。
如果你拥有一个值,例如,你可以将其所有权转移到另一个变量:
#![allow(unused)] fn main() { let a = 42; // <--- `a` 是值 `42` 的所有者 let b = a; // <--- `b` 现在是值 `42` 的所有者 }
Rust的所有权系统内置于类型系统中:每个函数都必须在其签名中声明它打算如何与其参数交互。
到目前为止,我们所有的方法和函数都消耗了它们的参数:它们获取了参数的所有权。 例如:
#![allow(unused)] fn main() { impl Ticket { pub fn description(self) -> String { self.description } } }
Ticket::description
获取调用它的Ticket
实例的所有权。这被称为移动语义:值(self
)的所有权从调用者转移到被调用者,调用者不能再使用它了。
这正是我们在前面看到的编译器错误信息中所使用的语言:
错误[E0382]: 使用了已移动的值: `ticket`
--> src/main.rs:30:43
|
25 | let ticket = Ticket::new(/* */);
| ------ 因为 `ticket` 类型为 `Ticket`,在此发生移动,
| 此类型未实现 `Copy` 特征
26 | if ticket.status() == "待办" {
| -------- `ticket` 因为此方法调用而被移动
...
30 | println!("你的下一个任务是: {}", ticket.title());
| ^^^^^^ 在移动后此处再次使用了值
|
注意: `Ticket::status` 接收者 `self` 采用所有权,导致 `ticket` 被移动
--> src/main.rs:12:23
|
12 | pub fn status(self) -> String {
| ^^^^
具体来说,当我们调用ticket.status()
时,事件序列如下:
Ticket::status
获取Ticket
实例的所有权Ticket::status
从self
提取status
并将status
的所有权转移回调用者- 剩余的
Ticket
实例部分被丢弃(title
和description
)
当我们尝试再次通过ticket.title()
使用ticket
时,编译器会抱怨:ticket
值现在已经没了,我们不再拥有它,因此不能再使用它。
要构建有用的访问器方法,我们需要开始使用引用。
借用
拥有一些不获取其所有权就能读取变量值的方法是可取的。否则编程将受到很大限制。在Rust中,这是通过借用来完成的。
每次你借用一个值时,都会得到它的引用。引用带有它们的权限标签1:
- 不可变引用(
&
)允许你读取值,但不允许修改它 - 可变引用(
&mut
)允许你读取并修改值
回到Rust所有权系统的目标:
- 数据在被读取时从不被修改
- 数据在被修改时从不被读取
为了确保这两点,Rust必须对引用引入一些限制:
- 你不能同时拥有对同一值的可变引用和不可变引用
- 你不能同时拥有对同一值的多个可变引用
- 所有者在值被借用期间不能修改值
- 只要有不可变引用,你可以拥有任意数量的不可变引用,只要没有可变引用
在某种程度上,你可以将不可变引用视为值上的“只读”锁,而可变引用则像是“读写”锁。
所有这些限制都由借用检查器在编译时强制执行。
语法
实际上如何借用一个值呢?
通过在变量前添加&
或&mut
,你就是在借用它的值。但要注意!相同的符号(&
和&mut
)在类型的前面有不同的含义:它们表示原始类型的引用,即引用类型本身。
例如:
struct 配置 { 版本: u32, 活动: bool, } fn main() { let 配置 = 配置 { 版本: 1, 活动: true, }; // `b` 是对 `config` 的 `版本` 字段的引用。 // `b` 的类型是 `&u32`,因为它包含对 `u32` 值的引用。 // 我们通过借用 `config.版本` 并使用 `&` 运算符创建引用。 // 同样的符号(`&`),根据上下文有不同的含义! let b: &u32 = &配置.版本; // ^ 类型注解不是必需的, // 它只是为了阐明正在发生的事情 }
同样的概念适用于函数参数和返回类型:
#![allow(unused)] fn main() { // `f` 接受一个 `u32` 的可变引用作为参数,绑定到名为 `number` fn f(number: &mut u32) -> &u32 { // [...] } }
深呼吸
Rust的所有权系统一开始可能会让人有些不知所措。但别担心:通过实践它会变得自然而然。在本章剩余部分以及整个课程中,你将获得大量的实践机会!我们将多次回顾每个概念,确保你熟悉它们并真正理解它们的工作原理。
在本章末尾,我们会解释为什么Rust的所有权系统设计成这样。目前,集中精力理解如何做。把每一个编译器错误都当作一次学习机会!
参考
- 本节练习位于
exercises/03_ticket_v1/06_ownership
这是一个很好的入门心理模型,但它没有捕捉到完整的画面。 我们将在课程的后续部分深化对引用的理解。