所有权和借用

Rust 最独特也最强大的特性之一就是它的 ​​ 所有权系统 ​​。这套系统在编译期就能帮你杜绝一大类内存错误,比如数据竞争和悬垂指针(引用指向了无效的内存)。理解所有权和借用规则是掌握 Rust 的关键。它们的核心思想其实挺简单的:


所有权规则 (Ownership Rules)

想象每个值都有一个“主人”。这套规则规定了主人和值的关系:

  1. ​ 每个值都有自己的主人:​​ 在 Rust 中,每一个值都必须被一个变量所拥有,这个变量就是它的 ​​ 所有者 ​​。
  2. ​ 一次只能有一个主人:​​ 一个值在同一个时间点,只能被一个变量所拥有。也就是说,​​ 一个值只有一个所有者 ​​。
  3. ​ 主人走了,值就没了:​​ 当作为所有者的 ​​ 变量离开它的作用域 ​​ 时,Rust 会自动调用 drop 函数,把这个值占用的内存清理掉(这个过程叫 ​​drop​​)。这保证了内存不会泄露。

借用规则 (Borrowing Rules)

直接转移所有权(比如通过函数传参)有时候不方便。Rust 提供了“借用”机制,让你能 ​​ 临时访问 ​​ 一个值,而不需要拿走所有权。借用是通过 ​​ 引用 ​​ (&) 和 ​​ 可变引用 ​​ (&mut) 来实现的。规则也很清晰:

  1. ​ 借东西也要讲规矩:​
    • 在 ​​ 任何给定时刻 ​​,对于同一块数据,你只能选择以下 ​​ 一种 ​​ 方式借用:
      • ​ 要么 ​​ 拥有 ​​ 一个 ​​ 可变的引用 (&mut)。
      • ​ 要么 ​​ 拥有 ​​ 任意多个 ​​ 不可变的引用 (&)。
    • 你不能同时有可变引用和不可变引用存在,也不能有多个可变引用同时活跃。(官方总结得非常到位:​​ 同一时刻,你只能拥有要么一个可变引用,要么任意多个不可变引用 ​)。
  2. ​ 借的东西必须有效:​​ 所有 ​​ 引用必须始终指向有效的内存 ​​。你不能拥有一个指向已经被释放的数据的引用(悬垂引用)。(官方原则:​​ 引用必须总是有效的 ​)。

为什么要这么设计?(内存安全!)

这些规则的核心目标就是为了解决两个臭名昭著的内存安全问题:

  • ​ 数据竞争 (Data Races):​​ 当多个线程同时访问同一块数据,且至少有一个线程在写,又没有同步机制时,就会发生数据竞争。结果可能是程序崩溃、数据损坏,或者出现完全预料之外的行为。Rust 的借用规则(规则 1)在编译期就几乎完全杜绝了数据竞争的可能性(在单线程上下文中,它也能防止意外的并发访问模式)。
  • ​ 悬垂引用 (Dangling References):​​ 引用指向的内存可能已经被释放了。访问这样的引用就像在悬崖边走钢丝,程序随时可能崩溃(Segmentation Fault)。所有权规则(规则 3)和借用规则(规则 2)共同确保了引用指向的数据在引用存活期间一定是有效的。

规则是如何防止问题的?看例子!

例子 1:违反规则 - 多个可变引用 (数据竞争风险)

1
2
3
fn main() {
let mut vec = vec;
}
  • ​ 问题:​​ 这段代码违反了借用规则 1:不能同时拥有多个活跃的可变引用 (ref1ref2 都想独占 vec)。
  • ​ 编译器会报错:​​ Rust 编译器会阻止你编译这段代码。
  • ​ 修复:​​ 通常需要调整代码结构,确保同一时间只有一个可变引用在使用。比如,去掉其中一个 println! 中对 ref1ref2 的使用。

例子 2:违反规则 - 同时借用可变和不可变 (数据竞争风险)

1
2
3
4
fn main() {
let mut vec = vec 存活时尝试借可变引用
println!("ref1: {:?}, ref2: {:?}", ref1, ref2);
}
  • ​ 问题:​​ 违反了借用规则 1:不能同时拥有活跃的不可变引用 (ref1) 和可变引用 (ref2)。为什么不行?
    • 如果有人通过 ref2(可变引用)修改了 vec(比如 ref2.push(7)),那么 ref1(不可变引用)读取到的值就变得不可预测了,这违背了不可变引用“只读”的承诺。
    • 官方文档解释得很形象:​​ 正在借用不可变引用的用户,肯定不希望他借用的东西,被另外一个人莫名其妙改变了。多个不可变借用被允许是因为没有人会去试图修改数据,每个人都只读这一份数据而不做修改,因此不用担心数据被污染。​
  • ​ 编译器会报错:​​ 同样编译不过。

例子 3:作用域的改变 (编译器越来越聪明了!)

早期版本的 Rust 编译器(Rust 1.31 之前)中,引用的作用域和它绑定的变量作用域是一样的(到花括号 } 结束)。这有时候写起来挺别扭:

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let mut s = String::from("hello");

let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2); // 在这里,r1 和 r2 的使命完成了
// 在早期编译器看来,r1 和 r2 的作用域要持续到 main 函数结束的花括号

let r3 = &mut s; // 早期编译器:错误!认为 r1 和 r2 (不可变) 还活着,不能借可变
println!("{}", r3);
} // 早期编译器:认为 r1, r2, r3 作用域在这里结束
  • ​ 老问题:​​ 在老编译器眼里,r1r2 的作用域一直到 main 函数结尾的 },所以在 println! 之后尝试创建可变引用 r3 会报错(违反了规则 1:同时存在不可变和可变借用)。
  • ​ 新改进:​​ 现代 Rust 编译器(引入了 ​​Non-Lexical Lifetimes, NLL​​)变得更聪明了!它 ​​ 判断引用的作用域结束于它最后一次被使用的位置 ​​(last use)。在上面的代码中:
    • r1r2println!("{} and {}", r1, r2); 之后就不再被使用了。
    • 编译器认为它们的借用作用域 ​​ 到此结束 ​​。
    • 因此,紧接着创建可变引用 r3 是允许的,代码能顺利编译通过。
  • ​ 新作用域:​
1
2
3
4
5
6
7
8
9
10
fn main() {
let mut s = String::from("hello");

let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2); // r1, r2 的借用作用域在此结束 (NLL)

let r3 = &mut s; // 允许!因为 r1, r2 的借用已经结束了
println!("{}", r3); // r3 的借用作用域在此结束 (NLL)
}

​ 总结:​​ Rust 的所有权和借用规则初看可能有点严格,但它们正是 Rust 实现内存安全并发且无需垃圾回收的核心武器。编译器是你的好帮手,它会严格执行这些规则。理解并习惯这些规则后,你会发现在它们约束下写出的代码,天然就具备更高的安全性和可靠性。


注意:该文章由 DeepSeek R1 结合课程笔记优化生成,并由 Garusuta 修改发布