为什么不能用第二次
一句话总结
Rust **强制每个值只能有一个所有者。**把值交给其他地方后,原来的位置就不能再使用它。仅凭这一条规则,Rust 就在没有 GC 的情况下实现了内存安全。
为什么需要它
C 系语言中有三类典型事故。
- use after free——再次使用已经释放的内存
- double free——重复释放同一块内存
- data race——两个线程同时修改同一个值
三者都源于“谁是这个值的所有者”并不明确。GC 可以消除前两类问题,但会付出暂停时间和内存成本,第三类问题仍然存在。
Rust 选择了另一种答案——**在编译阶段追踪所有者。**运行时开销为零。
只有三条规则
- 每个值只有一个所有者(owner)
- 所有者消失时,值也随之消失
- 把值**交给(move)**其他地方后,原所有者就失去了它
let s = String::from("hi");
let t = s; // 주인이 t 로 옮겨졌다
println!("{}", s); // ❌ 컴파일 오류: s 는 이미 넘겨졌다
错误消息会准确说明这一点。
error[E0382]: borrow of moved value: `s`
= note: move occurs because `s` has type `String`,
which does not implement the `Copy` trait
**实现 Copy 的类型会被复制。**整数、布尔值、字符等类型都属于这一类。因此,let a = 5; let b = a; 没有问题,因为它们大小固定,也不使用堆内存。
借用
如果只想暂时使用而不转移所有权,可以进行借用(borrow)。
fn len(s: &String) -> usize { s.len() } // 읽기만 빌림
let s = String::from("hi");
println!("{} {}", len(&s), s); // ✅ s 는 여전히 내 것
借用还有一条规则。
可以同时存在多个只读借用,但可写借用只能有一个,而且在此期间不能进行只读借用。
let mut v = vec![1, 2, 3];
let first = &v[0]; // 읽기 빌림
v.push(4); // ❌ 쓰기 빌림이 필요한데 읽기가 살아 있다
println!("{}", first);
这条规则阻止的是真实事故。如果 push 导致向量重新分配,first 指向的地址就会失效——在 C++ 中,这段代码可能悄无声息地通过,随后读出异常值。
生命周期
借用不能比原始值存活得更久,编译器会检查这一点。
fn dangle() -> &String { // ❌
let s = String::from("hi");
&s // s 는 함수가 끝나면 사라진다
}
大多数情况下,生命周期都可以自动推断。显式编写 'a 的场景比想象中少,主要用于说明多个引用中究竟与哪一个生命周期关联。
常见误解
“直接 clone 不就行了”——可以,而且很多时候这正是正确答案。与其花一整天和所有权搏斗,不如先用 clone() 让程序继续工作。如果性能真的成为问题,再查看分析器并进行优化。从一开始就试图用引用进行优化,是 Rust 显得困难的最常见原因。
“unsafe 就是不好的”——unsafe 表示“编译器无法在这里完成检查,因此由我来保证安全”。标准库内部也大量使用它。问题不在于使用 unsafe,而在于没有说明为什么它是安全的。
解决所有权问题的工具箱
与其和借用检查器对抗,不如根据场景选择合适的工具,大多数问题都会迎刃而解。
| 场景 | 工具 | 代价 |
|---|---|---|
| 暂时只读 | &T |
无 |
| 暂时修改 | &mut T |
期间不能存在其他引用 |
| 转移所有权 | 直接传值 | 原变量无法再使用 |
| 复制成本很低 | .clone() |
复制成本 |
| 多处共同持有 | Rc<T>(单线程) |
引用计数、循环泄漏 |
| 多线程共享 | Arc<T> |
原子计数(稍慢) |
| 共享且需要修改 | Arc<Mutex<T>> |
锁成本、可能死锁 |
**初学者使用 .clone() 并没有错。**先让程序正确运行;如果分析器指出复制是瓶颈,再改用引用。这比从一开始就与生命周期标注搏斗并最终放弃要好得多。
阅读编译错误的顺序
Rust 的错误消息虽然很长,但结构固定。
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src/main.rs:4:5
|
3 | let first = &v[0]; ← 여기서 불변으로 빌렸고
| - immutable borrow occurs here
4 | v.push(4); ← 여기서 가변으로 빌리려 한다
| ^^^^^^^^^ mutable borrow occurs here
5 | println!("{}", first); ← 불변 빌림이 여기까지 살아 있다
| ----- immutable borrow later used here
按顺序阅读三个箭头——在哪里发生借用,在哪里产生冲突,以及为什么借用仍然存活。最后一行最关键。如果不再使用 first,借用就会在 push 之前结束,错误也会消失(NLL,non-lexical lifetimes)。
运行 rustc --explain E0502 可以查看该错误的完整说明。每项说明都附有代码示例,第一次遇到某个错误时,先阅读这里通常最快。
什么时候使用 unsafe
几乎不需要使用。不过,unsafe 并不是“关闭检查”,而是表示**“我向编译器承诺会维持不变量”**。它只额外允许五类操作(如解引用原始指针、调用 unsafe 函数等),其他检查仍然有效。
实际工作中通常会在三个场景遇到它:调用 C 库(FFI)、自行实现数据结构(双向链表),以及经过证明的性能瓶颈。
**应尽量缩小 unsafe 块,并在其外层封装安全 API。**这样人工只需审查这个小块,使用者看到的则始终是安全接口。标准库正是采用这种结构。
实务中真正重要的事
养成把错误消息读完的习惯,是学习 Rust 的一半。rustc 通常连修复方法都会告诉你。
help: consider cloning the value if the performance cost is acceptable
|
5 | let t = s.clone();
| ++++++++
此外,cargo clippy 会指出虽然能够编译,但存在更好写法的位置。学习期间持续启用 clippy,效果近似于多读一本书。