Rust 学习笔记 05:所有权 (Ownership) 上
“I thought I knew what ownership meant until I met the borrow checker.” – Anonymous Rustacean
终于来到了 Rust 的核心 —— 所有权 (Ownership)。
作为 Go 开发者,我们习惯了 GC(垃圾回收)帮我们打理一切。我们随手创建一个指针,传给函数,传给 Channel,从来不需要关心它什么时候被释放。因为有 GC 在兜底。
但在 Rust 里,没有 GC。但它也没有让我们像 C++ 那样手动 malloc/free。
那它是怎么管理内存的?
答案就是:所有权系统 + 编译器静态检查。
1. 核心原则
所有权规则非常霸道,但只有三条:
- Rust 中的每一个值都有一个被称为其 所有者 (owner) 的变量。
- 值在任一时刻有且只有一个所有者。
- 当所有者(变量)离开作用域,这个值将被丢弃 (Drop),内存被释放。
这听起来很像作用域管理,但最关键的是第二条:有且只有一个所有者。
2. 移动 (Move) 语义
在 Go 中,变量赋值默认是值拷贝(对于指针是拷贝地址)。
1// Go 代码
2s1 := "hello"
3s2 := s1
4// 现在 s1 和 s2 都指向同一个字符串,随便用
但在 Rust 中,对于复杂类型(如 String,在堆上分配),情况完全不同:
1let s1 = String::from("hello");
2let s2 = s1;
3
4// println!("{}, world!", s1); // 编译报错!!
编译器会告诉你:borrow of moved value: s1。
发生了什么?
当 let s2 = s1 执行时,s1 的所有权 (Ownership) 转移到了 s2。
s1 变成了废纸,不能再被使用了。
这叫 Move (移动)。
为什么要这么设计?
为了避免 Double Free (二次释放) 错误。如果 s1 和 s2 都指向同一块堆内存,当它们离开作用域时,都会尝试释放那块内存。Rust 规定 s1 失效,只有 s2 负责释放,完美解决了这个问题。
3. 克隆 (Clone)
如果你真的想把数据复制一份(深拷贝),必须显式调用 clone()。
1let s1 = String::from("hello");
2let s2 = s1.clone();
3
4println!("s1 = {}, s2 = {}", s1, s2); // 正常工作
Go 的开发者注意:Rust 中的赋值(对于堆数据)默认是 Move,不是浅拷贝。
4. 拷贝 (Copy)
等等,为什么下面这段代码没报错?
1let x = 5;
2let y = x;
3println!("x = {}, y = {}", x, y); // x 依然有效
因为整数、布尔值等简单类型,大小固定且存储在栈上。它们实现了 Copy Trait。对于这些类型,赋值就是简单的位拷贝,速度极快,不需要转移所有权。
5. 函数传参
函数传参等同于变量赋值。
1fn main() {
2 let s = String::from("hello");
3 takes_ownership(s);
4 // s 在这里已经无效了!所有权交给了函数
5 // println!("{}", s); // 报错
6}
7
8fn takes_ownership(some_string: String) {
9 println!("{}", some_string);
10} // 函数结束,some_string 离开作用域,内存被释放
在 Go 里,我们习惯传指针进去修改,或者传值进去读。但在 Rust 里,如果你把一个堆对象传给函数(不引用),那就是把身家性命都交出去了,函数调用完,那个对象就没了。
6. 小结
第五篇笔记,我们接触了 Rust 最核心的“霸王条款”:
- 所有权独占:一山不容二虎,一个值同时只能有一个 Owner。
- Move 语义:赋值和传参默认是移动所有权,原变量直接失效(针对非 Copy 类型)。
这确实很反直觉。你想想,我这就传个参打印一下,怎么回来对象就没了?
别急,Rust 提供了 引用 (References) 和 借用 (Borrowing) 来解决这个问题。这就是我们下一篇(下集)的内容。
练习题:
- 定义一个
String,尝试把它赋值给另一个变量,然后打印原变量,观察编译器的报错。 - 写一个函数,接收一个
String,返回该String的长度,并把String的所有权还给调用者(提示:返回元组(String, usize))。
思考题:
Go 语言通过 GC 解决了内存释放问题,但也带来了 STW 和运行时开销。Rust 的 Move 语义虽然增加了编码负担,但它在运行时是零开销的。你觉得这种交换值得吗?
本文代码示例:
关注公众号:极客老墨
更多 AI 应用开发、工程实践和效率工具分享,欢迎扫码关注。
