所有权和借用
Rust 最独特也最强大的特性之一就是它的 所有权系统 。这套系统在编译期就能帮你杜绝一大类内存错误,比如数据竞争和悬垂指针(引用指向了无效的内存)。理解所有权和借用规则是掌握 Rust 的关键。它们的核心思想其实挺简单的:
所有权规则 (Ownership Rules)
想象每个值都有一个“主人”。这套规则规定了主人和值的关系:
- 每个值都有自己的主人: 在 Rust 中,每一个值都必须被一个变量所拥有,这个变量就是它的 所有者 。
- 一次只能有一个主人: 一个值在同一个时间点,只能被一个变量所拥有。也就是说, 一个值只有一个所有者 。
- 主人走了,值就没了: 当作为所有者的 变量离开它的作用域 时,Rust 会自动调用
drop函数,把这个值占用的内存清理掉(这个过程叫 drop)。这保证了内存不会泄露。
借用规则 (Borrowing Rules)
直接转移所有权(比如通过函数传参)有时候不方便。Rust 提供了“借用”机制,让你能 临时访问 一个值,而不需要拿走所有权。借用是通过 引用 (&) 和 可变引用 (&mut) 来实现的。规则也很清晰:
- 借东西也要讲规矩:
- 在 任何给定时刻 ,对于同一块数据,你只能选择以下 一种 方式借用:
- 要么 拥有 一个 可变的引用 (
&mut)。 - 要么 拥有 任意多个 不可变的引用 (
&)。
- 要么 拥有 一个 可变的引用 (
- 你不能同时有可变引用和不可变引用存在,也不能有多个可变引用同时活跃。(官方总结得非常到位: 同一时刻,你只能拥有要么一个可变引用,要么任意多个不可变引用 )。
- 在 任何给定时刻 ,对于同一块数据,你只能选择以下 一种 方式借用:
- 借的东西必须有效: 所有 引用必须始终指向有效的内存 。你不能拥有一个指向已经被释放的数据的引用(悬垂引用)。(官方原则: 引用必须总是有效的 )。
为什么要这么设计?(内存安全!)
这些规则的核心目标就是为了解决两个臭名昭著的内存安全问题:
- 数据竞争 (Data Races): 当多个线程同时访问同一块数据,且至少有一个线程在写,又没有同步机制时,就会发生数据竞争。结果可能是程序崩溃、数据损坏,或者出现完全预料之外的行为。Rust 的借用规则(规则 1)在编译期就几乎完全杜绝了数据竞争的可能性(在单线程上下文中,它也能防止意外的并发访问模式)。
- 悬垂引用 (Dangling References): 引用指向的内存可能已经被释放了。访问这样的引用就像在悬崖边走钢丝,程序随时可能崩溃(Segmentation Fault)。所有权规则(规则 3)和借用规则(规则 2)共同确保了引用指向的数据在引用存活期间一定是有效的。
规则是如何防止问题的?看例子!
例子 1:违反规则 - 多个可变引用 (数据竞争风险)
1 | fn main() { |
- 问题: 这段代码违反了借用规则 1:不能同时拥有多个活跃的可变引用 (
ref1和ref2都想独占vec)。 - 编译器会报错: Rust 编译器会阻止你编译这段代码。
- 修复: 通常需要调整代码结构,确保同一时间只有一个可变引用在使用。比如,去掉其中一个
println!中对ref1或ref2的使用。
例子 2:违反规则 - 同时借用可变和不可变 (数据竞争风险)
1 | fn main() { |
- 问题: 违反了借用规则 1:不能同时拥有活跃的不可变引用 (
ref1) 和可变引用 (ref2)。为什么不行?- 如果有人通过
ref2(可变引用)修改了vec(比如ref2.push(7)),那么ref1(不可变引用)读取到的值就变得不可预测了,这违背了不可变引用“只读”的承诺。 - 官方文档解释得很形象: 正在借用不可变引用的用户,肯定不希望他借用的东西,被另外一个人莫名其妙改变了。多个不可变借用被允许是因为没有人会去试图修改数据,每个人都只读这一份数据而不做修改,因此不用担心数据被污染。
- 如果有人通过
- 编译器会报错: 同样编译不过。
例子 3:作用域的改变 (编译器越来越聪明了!)
早期版本的 Rust 编译器(Rust 1.31 之前)中,引用的作用域和它绑定的变量作用域是一样的(到花括号 } 结束)。这有时候写起来挺别扭:
1 | fn main() { |
- 老问题: 在老编译器眼里,
r1和r2的作用域一直到main函数结尾的},所以在println!之后尝试创建可变引用r3会报错(违反了规则 1:同时存在不可变和可变借用)。 - 新改进: 现代 Rust 编译器(引入了 Non-Lexical Lifetimes, NLL)变得更聪明了!它 判断引用的作用域结束于它最后一次被使用的位置 (
last use)。在上面的代码中:r1和r2在println!("{} and {}", r1, r2);之后就不再被使用了。- 编译器认为它们的借用作用域 到此结束 。
- 因此,紧接着创建可变引用
r3是允许的,代码能顺利编译通过。
- 新作用域:
1 | fn main() { |
总结: Rust 的所有权和借用规则初看可能有点严格,但它们正是 Rust 实现内存安全并发且无需垃圾回收的核心武器。编译器是你的好帮手,它会严格执行这些规则。理解并习惯这些规则后,你会发现在它们约束下写出的代码,天然就具备更高的安全性和可靠性。
注意:该文章由 DeepSeek R1 结合课程笔记优化生成,并由 Garusuta 修改发布