Rust 核心解密:驯服“野牛”!一文搞懂借用与引用的生死规则
Rust 核心解密:驯服“野牛”!一文搞懂借用与引用的生死规则
摘要:为什么明明只是打印一个变量,编译器却报错?为什么可变引用不能复制?本文将深入 Rust 的借用机制,揭开不可变引用与可变引用的“互斥”真相,带你真正驯服这头内存安全的野牛。
在上节课中,我们认识了 Rust 的灵魂——所有权(Ownership)。我们知道,当所有权发生转移(Move)时,原变量就会失效。但这带来了一个巨大的麻烦:如果我想把数据传给函数用一下,但之后还想继续用它,难道每次都要把所有权传进去再传出来吗?
这太繁琐了。为了解决这个问题,Rust 引入了**借用(Borrowing)**的概念。
今天,我们将深入探讨 Rust 中最具挑战性、也最精妙的设计:引用(Reference)与借用规则。准备好你的键盘,我们要开始驯服这头“野牛”了。
01. 什么是借用?
在现实生活中,如果你有一本书,朋友想看看,你可以借给他。书还是你的,他只是暂时持有。
在 Rust 中:
- 借用(Borrowing):是行为。你把资源借给别人用。
- 引用(Reference):是工具。别人手里拿到的那个“借条”,就是引用。
在 Rust 代码中,使用 & 符号创建引用:
1 | let a = 10u32; |
引用的本质
引用本身也是一个值,而且是一个固定尺寸的值(通常等于机器字长,如 64 位)。因此,引用可以被随意复制和赋值,而不会触发所有权的移动。
1 | let s1 = String::from("Hello"); |
此时,s1 依然拥有字符串的所有权,s2, s3, s4 只是指向它的“观察者”。
02. 不可变引用 vs 可变引用
Rust 将引用分为两类,这与变量的可变性一脉相承:
- 不可变引用 (
&T):默认类型。只能读,不能写。就像借书给朋友,规定“只能看,不能涂改”。 - 可变引用 (
&mut T):需要显式声明。既能读,也能写。就像借书给朋友,允许他“做笔记”。
如何使用可变引用?
要修改通过引用指向的值,必须满足两个条件:
- 原始变量必须声明为
mut。 - 引用必须声明为
&mut。 - 解引用时使用
*操作符。
1 | fn main() { |
03. 借用检查器的三条铁律
这是 Rust 初学者最容易撞墙的地方。Rust 编译器(借用检查器)为了保证内存安全和数据竞争自由,强制执行以下三条规则:
规则一:同一时刻,要么有多个不可变引用,要么只有一个可变引用
不可变引用可以共存:
1 | let s = String::from("Hello"); |
可变引用具有排他性:
1 | let mut s = String::from("Hello"); |
不可变与可变不能共存:
1 | let mut s = String::from("Hello"); |
💡 为什么?
如果允许同时存在不可变和可变引用,当可变引用修改了数据时,不可变引用持有的数据就变成了“脏数据”或“悬空指针”,导致未定义行为。Rust 在编译期就杜绝了这种数据竞争(Data Race)。
规则二:引用的作用域是从定义处到最后一次使用处
这是 Rust 1.31 版本引入的非词法生命周期(NLL, Non-Lexical Lifetimes)特性。引用的作用域不再仅仅看花括号,而是看你最后在哪里用了它。
1 | fn main() { |
如果没有 NLL,上面的代码在传统词法作用域下会报错,因为 r1 和 r2 的作用域被认为一直延伸到块末尾。现在,Rust 足够聪明,知道它们用完就可以释放借用了。
规则三:只要有引用存在,所有者就不能直接修改数据
1 | let mut a = 10; |
即使 r 是不可变引用,只要它还存在,编译器就不允许你通过所有者 a 去修改数据,以防止 r 看到不一致的状态。
04. 可变引用的“移动”语义
这是一个容易混淆的点:不可变引用可以 Copy,但可变引用只能 Move。
1 | let mut a = 10; |
可变引用被视为资源的独家代理。既然同一时刻只能有一个可变引用,那么当你把 r1 赋值给 r2 时,实际上是把“独家修改权”移交给了 r2,r1 随即失效。
05. 多级引用与解引用
Rust 支持多级引用,但在修改时需要小心解引用的层数。
1 | let mut a = 10; |
注意:如果引用链中任何一环是不可变引用,则最终无法通过该链修改数据。
1 | let mut a = 10; |
06. 用引用优化函数参数
回到开头的问题,使用引用可以让函数签名更清晰,性能更高效:
1 | // 接收不可变引用,承诺不修改 |
这种方式避免了所有权的转移,调用者依然保留对 s 的控制权。
07. 总结与心法
Rust 的借用规则初看繁琐,实则严谨。请记住以下心法:
- 读写分离:要么大家一起读(多个
&),要么一个人写(单个&mut),绝不能混用。 - 尽早释放:引用的作用域取决于最后一次使用。尽早结束引用的使用,可以给后续的可变借用腾出空间。
- 独占代理:可变引用是独家的,赋值即移动。
- 安全第一:所有这些限制,都是为了在编译期消除数据竞争和悬空指针,让你写出无 Bug 的并发代码。




