深入理解 Rust 所有权与借用:建立简单而准确的心智模型
所有权(Ownership)与借用(Borrowing)是 Rust 语言最重要的特性之一,也是初学 Rust 时最容易产生困惑的部分。
例如,为什么下面的代码无法编译?
let s1 = String::from("hello");
let s2 = s1;
println!("{s1}"); // 编译错误
为什么这段代码却完全合法?
let s1 = String::from("hello");
let s2 = &s1;
println!("{s1}");
println!("{s2}");
再比如,为什么两个共享引用可以同时存在,而共享引用和可变引用却有可能发生冲突?为什么有时候一个引用变量仍在作用域内,Rust 却允许重新修改原变量?
这些现象看起来涉及许多规则,但实际上,它们可以通过一个统一的心智模型来理解:
数据有主人,主人可以将数据暂时借出去。共享借用允许多人读取,独占借用允许修改;借用结束后,主人可以继续正常使用数据。
下面我们从所有权开始,逐步建立这套模型。
一、所有权:每个值都有自己的主人
在 Rust 中,每个值都有一个所有者(Owner)。
Rust 的所有权机制遵循三个基本原则:
- 每个值都有一个所有者。
- 在任意时刻,一个值只有一个所有者。
- 当所有者离开作用域时,它所拥有的值会被清理,除非所有权已经转移。
考虑:
fn main() {
let s = String::from("hello");
println!("{s}");
}
这里,变量 s 拥有一个 String 值。
String 管理着字符串所使用的堆内存。当 main 函数结束时,s 离开作用域,Rust 会自动调用相应的析构逻辑,释放字符串占用的资源。
可以简单理解为:
s
│
│ 拥有 String
▼
"hello"
作用域结束
↓
String 被销毁
↓
所管理的堆内存被释放
整个过程不需要程序员手动调用 free(),也不依赖传统的垃圾回收器。
Rust 在编译阶段检查所有权规则,并在适当位置自动执行资源清理。
1.1 所有权可以转移:Move
考虑:
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{s2}");
}
这里:
let s2 = s1;
并没有创建一份新的堆字符串数据,而是将 String 的所有权从 s1 转移到 s2。
这种行为称为所有权移动(Move)。
移动之前:
s1 ──────> String("hello")
执行 let s2 = s1;
移动之后:
s1 不再能够使用
s2 ──────> String("hello")
因此:
println!("{s1}");
会发生编译错误。
这是因为 s1 已经失去了对该 String 值的所有权。
需要注意,Move 首先是一种语言层面的所有权语义,并不意味着底层机器一定没有发生任何字节复制。关键在于:移动完成后,原来的变量不能再作为该值的有效所有者使用。
1.2 为什么整数赋值不会导致原变量失效?
考虑:
let a = 100;
let b = a;
println!("{a}");
println!("{b}");
这段代码完全合法。
原因是 i32 实现了 Copy Trait。
对于实现了 Copy 的类型,赋值会产生独立的值副本,原变量仍然有效。
因此:
let a = 100;
let b = a;
与:
let s1 = String::from("hello");
let s2 = s1;
虽然语法相似,但所有权语义不同。
| 类型 | 赋值行为 | 原变量能否继续使用 |
|---|---|---|
i32 |
Copy | 可以 |
bool |
Copy | 可以 |
[i32; 30] |
Copy | 可以 |
String |
Move | 不可以 |
Vec<T> |
Move | 不可以 |
这里需要记住:
所有权移动和数据复制是不同的概念。某个类型是否实现 Copy,决定了普通赋值是否保留原变量的有效性。
二、可变性:let 和 let mut 的区别
在学习借用之前,我们先理解变量的可变性(Mutability)。
Rust 中的变量默认是不可变的。
例如:
let x = 100;
x = 200; // 编译错误
如果希望修改变量,需要使用 mut:
let mut x = 100;
x = 200; // 正确
对于 String 也一样:
let mut s = String::from("hello");
s.push_str(" world");
这里的 mut 允许我们通过变量 s 修改字符串。
但是,必须区分两个概念:
- 所有权:谁拥有这个值?
- 可变性:当前绑定是否允许修改这个值?
拥有一个值,并不意味着当前绑定一定能够修改它。
同样,一个值曾经被不可变绑定持有,也不意味着它以后永远不能修改。
例如:
fn add_suffix(mut text: String) -> String {
text.push_str(" world");
text
}
fn main() {
let s = String::from("hello");
let result = add_suffix(s);
println!("{result}");
}
这段代码合法。
虽然 main 中的 s 是不可变绑定,但调用:
add_suffix(s)
会将 String 的所有权转移给函数参数 text。
而函数参数:
mut text: String
是一个新的可变绑定,因此可以修改该字符串。
这说明:
mut 约束的是通过某个绑定所允许的操作,而不是给数据添加永久不可改变的属性。
不过,声明为 mut 并不意味着随时都可以修改数据。如果数据正在被借用,还必须遵守借用规则。
三、借用:不转移所有权,也能访问数据
现在我们已经知道,String 在按值赋值或传参时会发生 Move。
但是,这带来了一个问题:
如果我们只是希望函数读取字符串,而不希望函数取得字符串的所有权,该怎么办?
考虑:
fn print_string(s: String) {
println!("{s}");
}
fn main() {
let s = String::from("hello");
print_string(s);
println!("{s}"); // 编译错误
}
函数 print_string 按值接收 String,因此调用时会转移所有权。
但打印字符串并不需要拥有它。
为了解决这个问题,Rust 提供了引用(Reference)。
3.1 共享引用:&T
我们可以修改函数:
fn print_string(s: &String) {
println!("{s}");
}
fn main() {
let s = String::from("hello");
print_string(&s);
println!("{s}"); // 正确
}
这里:
&s
创建了对 s 的共享引用。
函数参数:
s: &String
表示接收一个 String 的共享引用,而不是接收 String 的所有权。
因此:
main 函数
s ──────────> String("hello")
▲
│
│ 借用
│
print_string
&String
函数可以通过引用读取字符串,但不会取得该字符串的所有权。
当函数调用结束后,原变量 s 依然有效。
这一过程称为借用(Borrowing)。
借用的本质是:在不转移数据所有权的情况下,暂时获得访问数据的能力。
对于只需要读取字符串内容的函数,实际开发中通常更推荐使用 &str 参数:
fn print_string(s: &str) {
println!("{s}");
}
这样既可以接受 String 的借用,也可以接受字符串切片,接口更加灵活。
四、共享借用与可变借用:Rust 最重要的访问规则
Rust 提供两种最基本的引用:
&T
&mut T
它们分别表示共享引用和可变引用。
理解这两种引用的区别,是掌握 Rust 借用机制的关键。
4.1 共享引用:允许多个引用同时读取
考虑:
fn main() {
let s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{r1}");
println!("{r2}");
}
这段代码完全合法。
因为 r1 和 r2 都是共享引用。
它们可以同时读取同一份字符串数据。
String("hello")
▲ ▲
│ │
│ │
r1 r2
&s &s
由于普通共享引用不会允许我们直接修改目标数据,因此多个共享引用可以安全地共存。
由此得到第一条借用规则:
同一份数据可以同时存在多个共享引用
&T。
4.2 可变引用:修改数据需要独占访问
如果我们希望通过引用修改数据,可以使用可变引用:
fn main() {
let mut s = String::from("hello");
let r = &mut s;
r.push_str(" world");
println!("{r}");
}
这里:
let r = &mut s;
创建一个可变引用。
它允许通过 r 修改 s 所拥有的字符串。
但是,可变引用有一条重要限制:
在可变借用有效期间,不能存在与它冲突的其他访问。
例如:
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
r1.push_str(" world");
r2.push_str("!");
}
这段代码无法编译。
原因是 r1 的可变借用仍然需要使用时,又创建了一个与它冲突的可变引用 r2。
如果允许两个可变引用独立地修改同一份数据,就可能产生难以推断的数据变化,并破坏 Rust 对引用独占性的保证。
因此,我们得到第二条借用规则:
&mut T提供独占访问能力,相互冲突的可变借用不能同时有效。
注意,可变引用并不要求引用变量自身也必须声明为 mut。
例如:
let mut x = 100;
let r = &mut x;
*r = 200;
完全合法。
因为:
mut r决定能否重新给引用变量r赋值。&mut i32决定能否通过引用修改目标数据。
它们并不是同一个概念。
4.3 为什么共享引用和可变引用不能冲突?
考虑:
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &mut s;
println!("{r1}");
}
这段代码无法编译。
原因是:
r1是共享引用。r2试图获得独占的可变访问权限。- 创建
r2时,r1在后面仍然需要使用。
如果允许这种操作,就意味着同一份数据既处于共享借用状态,又可以通过另一个引用修改。
这种行为可能破坏共享引用所依赖的数据稳定性。
例如,对于 Vec<T>,某些修改操作可能导致底层内存重新分配,使已有的元素引用失效。
因此,Rust 的普通引用规则可以概括为:
同一份数据,要么允许多个共享引用读取,要么允许一个可变引用独占访问;相互冲突的借用不能同时有效。
这也是 Rust 中经常提到的:
Aliasing XOR Mutability
即共享别名访问与独占可变访问之间的互斥关系。
这里讨论的是普通引用的基本模型。Rust 还存在内部可变性、重新借用等更深入的机制,但它们并不改变我们当前需要掌握的核心原则。
五、借用什么时候结束?
到目前为止,我们已经知道:
- 共享引用允许多个读取者。
- 可变引用需要独占访问。
- 相互冲突的借用不能同时有效。
但还有一个非常关键的问题:
一个引用的借用会一直持续到所在的大括号结束吗?
答案是:不一定。
现代 Rust 的借用检查器会分析引用实际需要保持有效的范围。
在简单的顺序代码中,我们可以先这样理解:
借用通常可以在引用最后一次使用之后结束。
5.1 第一段代码:无法编译
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &mut s;
println!("{r1}");
}
为什么无法编译?
分析执行顺序:
创建 s
│
▼
创建共享引用 r1
│
▼
创建可变引用 r2 ← 借用冲突
│
▼
继续使用 r1
问题在于:
创建 r2 的时候,r1 的共享借用仍然需要保持有效,因为后面还有:
println!("{r1}");
因此,不能创建与它冲突的 &mut s。
5.2 第二段代码:可以编译
现在调整两行代码的顺序:
fn main() {
let mut s = String::from("hello");
let r1 = &s;
println!("{r1}");
let r2 = &mut s;
}
这段代码可以编译。
为什么?
因为在创建 r2 之前,r1 已经完成最后一次使用。
创建 s
│
▼
创建共享引用 r1
│
▼
println!("{r1}")
│
▼
r1 最后一次使用
│
▼
共享借用结束
│
▼
创建可变引用 r2 ← 合法
虽然 r1 仍然位于 main 函数的大括号之内,但是它的共享借用已经不需要继续保持有效。
因此,Rust 允许之后创建可变引用。
这就是非词法生命周期(Non-Lexical Lifetimes,NLL)所支持的重要行为。
5.3 变量作用域和借用有效范围不是一回事
这里需要明确区分两个概念:
变量作用域(Scope):变量名可以在源代码的哪些位置被使用。
借用的有效范围:为了保证引用的使用安全,某次借用需要在程序的哪些位置保持有效。
两者并不完全相同。
例如:
fn main() {
let mut s = String::from("hello");
let r = &s;
println!("{r}");
s.push_str(" world");
println!("{s}");
}
虽然 r 的词法作用域仍然覆盖之后的代码,但它已经不再被使用,因此共享借用可以结束。
不过,“最后一次使用”只是理解简单顺序代码的便捷方式。
对于存在条件分支、循环等控制流的程序,Rust 会进行更精细的借用分析,而不是机械地根据源代码中的某一行决定生命周期。
5.4 可变借用也遵循同样的原则
例如:
fn main() {
let mut s = String::from("hello");
let r = &mut s;
r.push_str(" world");
println!("{r}");
println!("{s}");
}
这段代码完全合法。
因为在:
println!("{r}");
之后,r 不再被使用,因此对 s 的可变借用限制可以结束。
原来的 s 便可以继续正常访问。
需要特别注意:
借用结束并不意味着所有权被归还,因为所有权从未发生转移。
更准确地说,是借用造成的临时访问限制不再生效。
六、数据必须比引用存活得更久
前面讨论的规则主要解决一个问题:
同一份数据可以被哪些引用访问,以及什么时候允许修改?
但是,Rust 还需要保证另一个重要条件:
引用指向的数据,在引用被使用时必须仍然有效。
考虑:
fn main() {
let r;
{
let s = String::from("hello");
r = &s;
}
println!("{r}");
}
这段代码无法编译。
为什么?
因为变量 s 在内部作用域结束时就会被销毁,其管理的字符串数据也随之释放。
但程序却试图通过 r 继续访问 s。
内部作用域开始
│
▼
创建 String s
│
▼
r 引用 s
│
▼
内部作用域结束
│
▼
s 被销毁
│
▼
继续访问 r ← 危险
如果允许这种情况,就会产生悬垂引用(Dangling Reference)。
因此,Rust 在编译阶段拒绝这段代码。
我们可以把这一要求理解为:
借用的数据必须在整个有效借用期间保持存在。
这也是 Rust 生命周期(Lifetime)机制最根本的目的之一。
对于普通的局部借用,编译器通常能够自动推断生命周期,我们不需要手动编写任何生命周期标注。
只有在某些函数签名等更复杂的场景中,才需要使用显式的生命周期参数来描述引用之间的关系。
七、通过一段代码串联所有规则
现在,我们已经建立了完整的基础模型。
最后用一段程序,将所有权、可变性、共享借用、可变借用以及借用结束串联起来:
fn main() {
// 1. s 拥有 String,且允许修改
let mut s = String::from("hello");
// 2. 创建两个共享引用
let r1 = &s;
let r2 = &s;
// 3. 通过共享引用读取数据
println!("{r1}, {r2}");
// 4. 共享借用不再需要,创建可变引用
let r3 = &mut s;
// 5. 通过可变引用修改字符串
r3.push_str(" world");
// 6. 可变引用最后一次使用
println!("{r3}");
// 7. 借用结束,原变量继续使用
println!("{s}");
}
输出:
hello, hello
hello world
hello world
逐步分析:
第一步,s 创建并拥有一个 String 值。
第二步,r1 和 r2 共享借用该字符串。由于没有发生修改,因此两个共享引用可以同时存在。
第三步,在 println! 中读取 r1 和 r2,随后它们的共享借用不再需要继续存在。
第四步,创建可变引用 r3。由于之前的共享借用已经结束,因此不会发生冲突。
第五步,通过 r3 修改字符串。
第六步,r3 完成最后一次使用,其可变借用可以结束。
第七步,原变量 s 继续正常访问字符串。
整个过程中,String 的所有权始终属于 s,没有发生任何所有权移动。
引用只是暂时提供了访问字符串的能力。
这段代码正是 Rust 所有权与借用模型的典型体现。
八、最终总结:一个心智模型,三条借用规则
经过前面的分析,我们可以将 Rust 的所有权与借用归纳为几个简单的概念。
首先,所有权决定谁拥有数据。
一个值拥有一个所有者。对于没有实现 Copy 的类型,赋值或按值传参通常会移动所有权。
其次,可变性决定某个绑定是否允许修改数据。
let 默认创建不可变绑定,let mut 创建可变绑定。但是,即使变量可变,修改操作仍然需要满足借用规则。
最后,借用允许在不转移所有权的情况下访问数据。
它遵循三条核心规则:
规则一:共享引用可以共存。
let r1 = &s;
let r2 = &s;
多个共享引用可以同时读取同一份数据。
规则二:可变引用需要独占访问。
let r = &mut s;
在可变借用有效期间,不能存在与它冲突的其他借用或访问。
规则三:借用不再需要后,原变量可以继续正常访问。
let r = &s;
println!("{r}");
s.push_str(" world");
只要 r 的共享借用已经结束,便可以重新修改 s。
此外,还必须始终满足一条基本的安全要求:
引用指向的数据,在引用有效使用期间必须保持存在。
将这些知识归纳起来,我们便得到一个简单而准确的心智模型:
数据有主人,主人可以把数据暂时借出去。共享借用允许多人读取,独占借用允许修改;借用结束后,主人可以继续正常使用数据。同时,被借用的数据必须始终有效。
这套模型虽然简单,却已经能够解释 Rust 中大多数基础所有权与借用行为。
对于初学者,没有必要一开始就记忆复杂的权限图、生命周期推断算法或者借用检查器的内部实现。
更有效的学习方式,是每次面对一段 Rust 代码时,依次思考三个问题:
- 谁拥有这份数据?
- 现在有哪些引用正在借用它?这些引用是否发生冲突?
- 这些借用什么时候不再需要?
只要能够回答这三个问题,大多数 Rust 所有权与借用问题都会变得清晰。
当这套心智模型足够稳固之后,再深入学习生命周期标注、重新借用、内部可变性和智能指针,就会容易得多。

浙公网安备 33010602011771号