借用检查器不是敌人,它只是比我更早地发现了问题。
三条规则,一句话
Rust 所有权的规则只有三条:
- 每个值都有一个所有者
- 同一时刻只能有一个所有者
- 所有者离开作用域时,值被丢弃
看起来简单,真正让人卡住的是「借用」和「生命周期」这两层。
移动(move)到底发生了什么
rust
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1); // 编译错误:borrow of moved value1
2
3
4
2
3
4
很多人第一反应是「这不是浅拷贝吗」。不完全是。
String 在栈上存的是「指针 + 长度 + 容量」,真实字符数据在堆上。 赋值时 Rust 把栈上那三个字段复制过去,同时把 s1 标记为失效。
所以它不是浅拷贝(那样会导致双重释放),也不是深拷贝(那样有运行时开销), 而是移动:所有权转移,旧变量作废。
想保留原来的变量,用
s1.clone(),但要清楚这是显式付出了一次堆分配的成本。
借用:把「读」和「写」分开
借用规则可以概括成一句话:
要么有任意多个不可变借用,要么有恰好一个可变借用,两者不能同时存在。
rust
let mut v = vec![1, 2, 3];
let a = &v; // 不可变借用
let b = &v; // 再来一个,没问题
println!("{a:?} {b:?}"); // a、b 在这里之后就不再使用
let c = &mut v; // 现在可以可变借用了
c.push(4);1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
关键在最后一段:a 和 b 在 println! 之后就再也没有被使用, 所以它们的借用生命周期在那一行结束了。这就是所谓的 NLL(非词法生命周期)。
初学者最常犯的错是以为「借用会持续到作用域结束」,实际上编译器看的是 最后一次使用的位置。
什么时候该用 Rc 和 RefCell
如果你发现自己在跟借用检查器较劲,先别急着上 Rc<RefCell<T>>,问自己三个问题:
| 问题 | 如果是 | 该用 |
|---|---|---|
| 真的需要共享所有权吗? | 是 | Rc<T> / Arc<T> |
| 真的需要在共享的同时修改吗? | 是 | Rc<RefCell<T>> |
| 这个结构真的需要是图吗? | 否 | 重新设计数据布局 |
我的经验是:大部分时候答案是「不需要」。把数据拆开、把所有权理清楚, 代码往往比堆一层 RefCell 更清楚。
一点感想
写了几个月 Rust 之后,最大的收获不是学会了一门语言, 而是被迫把「这块内存归谁管」这件事想清楚了。
以前写 C++ 或者 Go,这些决定是隐式的,靠习惯和约定。 Rust 把它们摆到台面上,编译不过的时候你就得想明白。
想明白了,代码通常也就变简单了。