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. 核心原则

所有权规则非常霸道,但只有三条:

  1. Rust 中的每一个值都有一个被称为其 所有者 (owner) 的变量。
  2. 值在任一时刻有且只有一个所有者。
  3. 当所有者(变量)离开作用域,这个值将被丢弃 (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) 转移到了 s2s1 变成了废纸,不能再被使用了。

这叫 Move (移动)

为什么要这么设计? 为了避免 Double Free (二次释放) 错误。如果 s1s2 都指向同一块堆内存,当它们离开作用域时,都会尝试释放那块内存。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) 来解决这个问题。这就是我们下一篇(下集)的内容。


练习题

  1. 定义一个 String,尝试把它赋值给另一个变量,然后打印原变量,观察编译器的报错。
  2. 写一个函数,接收一个 String,返回该 String 的长度,并把 String 的所有权还给调用者(提示:返回元组 (String, usize))。

思考题

Go 语言通过 GC 解决了内存释放问题,但也带来了 STW 和运行时开销。Rust 的 Move 语义虽然增加了编码负担,但它在运行时是零开销的。你觉得这种交换值得吗?


本文代码示例

关注公众号:极客老墨

更多 AI 应用开发、工程实践和效率工具分享,欢迎扫码关注。

极客老墨微信公众号二维码

相关阅读