所有权

如果你按照本课程目前所学的内容解决了上一个练习,你的访问器方法可能看起来像这样:

#![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来说,以下所有内容同时都是真实的:

  1. 没有运行时垃圾收集器
  2. 作为开发者,你很少需要直接管理内存
  3. 你无法造成悬挂指针、重复释放以及其他与内存相关的错误

像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::statusself提取status并将status的所有权转移回调用者
  • 剩余的Ticket实例部分被丢弃(titledescription

当我们尝试再次通过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
1

这是一个很好的入门心理模型,但它没有捕捉到完整的画面。 我们将在课程的后续部分深化对引用的理解。