在 Rust 所有权系统 (Ownership System) 的框架下,引用 (References) 提供了一种在不转移所有权的情况下访问数据的方式,这个过程被称为 借用 (Borrowing)。Rust 区分两种主要类型的引用:不可变引用 (Immutable References)可变引用 (Mutable References)。这种区分以及它们各自严格的规则是 Rust 保证内存安全和并发安全的核心机制,尤其有效地防止了数据竞争 (Data Races)。

核心思想:引用允许安全地共享数据而不必转移所有权。Rust 的强类型系统和借用检查器严格区分不可变引用和可变引用,并强制执行“一可变或多不可变”的规则,从而在编译时消除数据竞争等常见内存错误。


一、引用的基本概念

引用是指向存储在内存中某个值的指针,但它不拥有该值。这意味着当引用离开作用域时,它所指向的值不会被丢弃。引用允许你在代码的不同部分之间共享数据,而无需担心所有权问题。

1.1 借用 (Borrowing)

创建引用被称为“借用”。就像现实生活中,你借用一本书,你可以阅读它(不可变借用),或者如果允许,你也可以在其中做笔记(可变借用),但你并不拥有这本书,最终你需要归还它。

二、不可变引用 (Immutable References) - &T

2.1 定义与语法

不可变引用使用 & 符号加上类型 T 来表示,例如 &String&i32。它允许你读取数据,但不能通过这个引用修改数据。

2.2 核心规则

  1. 可同时存在多个不可变引用:在任何给定时间,你可以拥有对同一数据任意数量的不可变引用。
  2. 不能通过不可变引用修改数据:如果你尝试通过不可变引用修改其引用的数据,编译器将报错。

2.3 目的与用途

  • 安全地共享数据:当多个部分需要同时读取相同的数据时,不可变引用是理想的选择。
  • 函数参数:常见于函数接收数据但不修改数据的情况,避免了不必要的内存复制和所有权转移。

2.4 代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn calculate_length(s: &String) -> usize { // s 是对 `String` 的不可变引用
s.len()
} // s 离开作用域,但不释放它引用的数据

fn main() {
let s1 = String::from("hello"); // s1 是所有者

let len = calculate_length(&s1); // 传入 s1 的不可变引用
println!("The length of '{}' is {}.", s1, len); // s1 仍然有效

let r1 = &s1; // 第一个不可变引用
let r2 = &s1; // 第二个不可变引用
println!("References: {} and {}", r1, r2); // r1 和 r2 都可以安全地访问 s1

// 尝试通过不可变引用修改数据会报错
// r1.push_str(" world"); // 编译错误: `r1` 是 `&String`, 这意味着它是一个不可变引用
}

三、可变引用 (Mutable References) - &mut T

3.1 定义与语法

可变引用使用 &mut 关键字加上类型 T 来表示,例如 &mut String&mut i32。它允许你通过这个引用修改数据。

3.2 核心规则

  1. 唯一性原则:在任何给定时间,你只能拥有对特定数据的 一个 可变引用。
  2. 排他性原则:当存在一个可变引用时,将 不能有任何其他引用 (无论是可变引用还是不可变引用) 指向同一数据。

3.3 目的与用途

  • 修改数据:当需要改变数据而不转移其所有权时,可变引用是必需的。
  • 防止数据竞争 (Data Races):这是可变引用最关键的规则。通过确保在写入数据时没有其他引用,Rust 在编译时消除了数据竞争这种难以调试的并发错误。

3.4 代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
fn change_string(s: &mut String) { // s 是对 `String` 的可变引用
s.push_str(", world!");
} // s 离开作用域

fn main() {
let mut s = String::from("hello"); // 必须使用 `mut` 关键字声明 `s` 为可变

change_string(&mut s); // 传入 s 的可变引用
println!("{}", s); // s 已被修改为 "hello, world!"

// 尝试创建多个可变引用 (编译错误)
// let r1 = &mut s;
// let r2 = &mut s; // 编译错误: 无法同时借用 `s` 作为可变,因为之前已经借用过
// println!("{}, {}", r1, r2);

// 尝试同时创建可变引用和不可变引用 (编译错误)
let ref1 = &s; // 不可变引用
// let ref2 = &mut s; // 编译错误: 无法同时借用 `s` 作为可变,因为之前已经借用过(不可变)
// println!("{}, {}", ref1, ref2);

// 正确的做法是确保引用的作用域不会重叠
{
let r1_mut = &mut s; // r1_mut 是唯一一个可变引用
r1_mut.push_str("!");
} // r1_mut 在这里离开作用域,现在可以创建新的引用了

let r2_imm = &s; // 创建一个新的不可变引用是允许的
println!("After mutable changes, s: {}", r2_imm);
}

四、不可变引用与可变引用的交互

Rust 对不可变引用和可变引用的管理是严格且互斥的:

  • 一个可变引用 独占性地访问数据。当它存在时,不能有任何其他引用(无论是可变还是不可变)指向同一数据。
  • 多个不可变引用 可以同时存在,因为它们不会修改数据,不会引起数据竞争。

这个模式可以用以下逻辑表示:

$$( \text{存在_可变引用} \land \forall \text{其他引用} (\text{不存在}) ) \lor ( \neg \text{存在_可变引用} \land \forall \text{其他引用} (\text{是_不可变}) )$$

简而言之:你要么只有一个可变引用,要么有任意数量的不可变引用,但绝不能同时拥有可变引用和任何其他引用。

交互图示

五、为什么 Rust 如此严格?—— 防止数据竞争 (Data Races)

数据竞争是一种严重的并发错误,它发生在以下三个条件同时满足时:

  1. 两个或更多指针同时访问同一数据。
  2. 至少一个指针正在写入数据。
  3. 没有同步机制来控制对数据的访问。

数据竞争会导致程序行为不可预测,产生难以复现和调试的错误。

Rust 的解决方案
Rust 的借用规则在编译时就强制排除了数据竞争发生的所有三个条件。

  • 条件 1 & 2:当你有一个可变引用 (&mut T) 时,它独占了对数据的写入权限,并且保证了没有其他指针同时访问数据。
  • 条件 3:Rust 的借用检查器充当了编译时的“同步机制”,确保所有访问模式都是安全的。

这意味着,只要你的 Rust 程序通过了编译,它就保证不会有数据竞争。这是 Rust 在没有垃圾回收器的情况下实现内存安全和并发安全的关键。

六、作用域与生命周期 (Lifetimes)

引用的有效性与它们所引用的数据的作用域和生命周期密切相关。Rust 编译器 (借用检查器) 会确保引用不会比其指向的数据活得更久,从而避免了 悬垂引用 (Dangling References)

例如,一个函数不能返回对局部变量的引用,因为当函数返回时,局部变量将被销毁,返回的引用将指向无效内存。

1
2
3
4
5
6
7
8
9
10
// 错误示例:返回一个悬垂引用
// fn dangling_reference() -> &String {
// let s = String::from("hello"); // s 在函数体内创建
// &s // 返回一个对 s 的引用
// } // s 在这里被丢弃,其内存被释放。返回的引用将指向无效内存。

// 除非返回数据的所有权,否则无法返回局部变量的引用
fn correct_return() -> String {
String::from("hello") // 返回 String 的所有权
}

通过生命周期注解,Rust 能够推断出复杂引用场景的有效性,并要求开发者在必要时显式声明,以满足编译器的检查。

七、总结

Rust 的可变引用和不可变引用是其所有权系统的重要组成部分,它们共同协作,在编译时提供了强大的内存安全和并发安全保障。

  • 不可变引用 (&T) 允许多个使用者同时读取数据,但不能修改。
  • 可变引用 (&mut T) 提供独占的访问权限,允许单个使用者修改数据,但排斥所有其他引用。

这种严格的规则虽然在初学时可能略显复杂,但它带来的回报是巨大的:在不引入运行时开销(如 GC)的情况下,有效地消除了 C/C++ 中常见的内存错误和并发设计中的数据竞争问题。掌握这两种引用及其规则,是编写高效、可靠且安全的 Rust 代码的基石。