第 6 章:准备好被虐了吗?

前面那些都是开胃菜。现在,真正的 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 的运行时开销,没有手动管理的出错风险。这就是所有权系统的威力。

三大定律:不容违反的物理规则

记住这三条定律,它们比牛顿定律更不容违反:

  1. 每个值都有且仅有一个所有者
  2. 同一时间只能有一个所有者
  3. 所有者离开作用域,值就被销毁

违反定律的后果?

编译器直接拒绝你的代码。没有商量余地。

实战演练: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 包含三部分:

  • 指针 - 指向堆上的数据
  • 长度 - 当前字符串长度
  • 容量 - 分配的总容量

如果简单复制,s1s2 会指向同一块堆内存。当它们离开作用域时:

双重释放!程序崩溃!安全漏洞!

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 的 Moves1 失效,只有 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 最优雅的特性了吗?