前面那些都是开胃菜。现在,真正的 Rust 来了。
所有权系统 —— 这就是 Rust 区别于其他一切语言的核心武器。它不是一个"功能",不是一个"库",而是一套不容违反的物理定律。
编译器就是执法官,你就是嫌疑人。
内存管理:三种境界
在我们开始之前,先上一堂"内存管理哲学课":
Java 的"舒适圈"
String s = new String("hello");
// 剩下的?GC 会搞定,你什么都不用想
舒适吗?当然。高效吗?呵呵。
GC 就像个保姆:你可以随意乱扔垃圾,他会定期来收拾。但代价是什么?
- 运行时开销 - 每次分配都有成本
- Stop-The-World - GC 运行时你的程序要停下来
- 内存占用 - 垃圾回收需要额外的内存开销
C/C++ 的"地狱模式"
char* s = malloc(100);
// 用完了记得 free(s)
// 忘了?内存泄漏
// 多次 free?程序崩溃
// 用了已释放的内存?未定义行为
性能极致,但是…一个错误就能让你的程序变成定时炸弹。
Rust 的"终极方案"
编译期确定所有内存的生命周期,运行时零开销。
没有 GC 的运行时开销,没有手动管理的出错风险。这就是所有权系统的威力。
三大定律:不容违反的物理规则
记住这三条定律,它们比牛顿定律更不容违反:
- 每个值都有且仅有一个所有者
- 同一时间只能有一个所有者
- 所有者离开作用域,值就被销毁
违反定律的后果?
编译器直接拒绝你的代码。没有商量余地。
实战演练:Copy vs Move
栈上数据:Copy 的天堂
let x = 5;
let y = x; // 复制,x 和 y 都有效
println!("x: {}, y: {}", x, y); // 完全没问题
为什么?因为 i32
完全存储在栈上,复制成本极低。Rust 允许这种廉价的复制。
堆上数据:Move 的规则
现在看看真正的挑战:
let s1 = String::from("hello");
let s2 = s1; // s1 被 move 了!
println!("{}", s1); // 编译错误!
为什么不能复制?
一个 String
包含三部分:
- 指针 - 指向堆上的数据
- 长度 - 当前字符串长度
- 容量 - 分配的总容量
如果简单复制,s1
和 s2
会指向同一块堆内存。当它们离开作用域时:
双重释放!程序崩溃!安全漏洞!
Rust 的解决方案?禁止你这么做。
let s2 = s1
之后,s1
就失效了。所有权移动到了 s2
。只有一个所有者,问题解决。
编译器的无情执法
试试违反规则:
let s1 = String::from("hello");
let s2 = s1; // s1 失效
println!("{}", s1); // 犯罪现场!
编译器的回应:
error[E0384]: borrow of moved value: `s1`
|
| let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`
| let s2 = s1;
| -- value moved here
| println!("{}", s1);
| ^^ value borrowed here after move
看到了吗?编译器比你更清楚谁拥有什么。
函数调用:所有权的转移站
函数调用也要遵守所有权规则:
fn main() {
let s = String::from("hello");
takes_ownership(s); // s 的所有权被夺走了
// println!("{}", s); // 编译错误!s 已经不存在了
}
fn takes_ownership(some_string: String) {
println!("{}", some_string);
} // some_string 在这里被销毁,堆内存被释放
函数就像黑洞,把你的数据吸走就不还了。
想要数据还回来?函数必须显式返回:
fn main() {
let s1 = String::from("hello");
let s2 = takes_and_gives_back(s1); // s1 失效,s2 获得所有权
println!("{}", s2); // 现在 s2 是主人
}
fn takes_and_gives_back(a_string: String) -> String {
a_string // 把所有权返回给调用者
}
麻烦吗?确实麻烦。但这种麻烦换来的是什么?
- 零运行时成本 - 没有 GC 暂停
- 内存安全 - 不会有内存泄漏或野指针
- 并发安全 - 数据竞争在编译期就被发现
深入理解:内存布局的真相
String 的内部构造
let s = String::from("hello");
内存布局:
栈上 s 变量:
┌─────────┬────────┬──────────┐
│ ptr │ len │ capacity │
│ ───────►│ 5 │ 5 │
└─────────┴────────┴──────────┘
│
▼
堆上数据: ┌───────────────────┐
│ h e l l o │
└───────────────────┘
当 let s2 = s1
时:
- 如果简单复制:两个指针指向同一块堆内存 → 双重释放灾难
- Rust 的 Move:
s1
失效,只有s2
指向堆内存 → 安全
这就是所有权系统的数学基础。
性能对比:Rust vs Java
Java 版本
List<String> list = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
list.add(new String("item" + i)); // 每次分配,GC 压力
}
// GC 运行,程序暂停
Rust 版本
let mut vec = Vec::new();
for i in 0..1000000 {
vec.push(format!("item{}", i)); // 精确的内存管理
}
// 离开作用域,所有内存立即释放,零开销
哪个更快?哪个更可预测?答案显而易见。
写在最后:痛苦与收获
学习所有权系统是痛苦的。Rust 编译器会是你见过最严厉的老师。
它会拒绝你的代码,它会逼你重新思考数据的生命周期,它会让你怀疑自己的编程能力。
但这种痛苦是值得的。
因为一旦你的代码通过了编译,你就获得了:
- 内存安全 - 永远不会有内存泄漏
- 并发安全 - 数据竞争不可能存在
- 性能极致 - 零运行时开销的抽象
- 重构信心 - 编译器保证你的修改不会破坏内存安全
这就是 Rust 的承诺:如果编译通过,程序就是安全的。
但你可能会问:每次想让函数用一下我的数据,就要把所有权转移来转移去,这也太麻烦了吧?
你说得对。确实麻烦。
幸好,Rust 提供了一个优雅的解决方案:借用系统。
下一章我们要学习如何"借用"数据而不转移所有权。这将彻底改变你使用 Rust 的方式。
准备好学习 Rust 最优雅的特性了吗?