智能指针:当单一所有权不够用时
你将学到什么:
Box<T>、Rc<T>、Arc<T>、RefCell<T>和Cow<'a, T>的使用场景,它们与 C# 中由 GC 管理的引用有什么区别,Drop如何对应 Rust 版的IDisposable,以及如何通过决策树选出合适的智能指针。难度: 高级
在 C# 里,几乎所有对象都处在 GC 管理下,可以被多个引用指向。而在 Rust 中,默认模型是单一所有权;但有些时候你确实需要共享所有权、显式堆分配,或者内部可变性。这就是智能指针登场的地方。
常见的智能指针
1. Box<T> (堆分配)
最基础的智能指针。当你需要把一个值存放在堆(Heap)上而不是栈(Stack)上时,就使用它。
- 使用场景: 递归数据结构(编译器无法在编译期确定其大小)。
#![allow(unused)]
fn main() {
enum List {
Cons(i32, Box<List>),
Nil,
}
}
2. Rc<T> (引用计数)
允许在单线程环境中,针对同一个数据拥有多个所有者。
- 使用场景: 图(Graph)节点或共享的配置对象。
#![allow(unused)]
fn main() {
let shared = Rc::new(vec![1, 2, 3]);
let a = Rc::clone(&shared);
let b = Rc::clone(&shared); // 引用计数现在为 3
}
3. Arc<T> (原子引用计数)
Rc<T> 的线程安全版本。
- 使用场景: 在多个线程之间共享数据。
#![allow(unused)]
fn main() {
let shared_data = Arc::new(vec![10, 20]);
thread::spawn(move || {
println!("{:?}", shared_data);
});
}
4. RefCell<T> (内部可变性)
即使你只持有指向容器的不可变引用 (&T),它也允许你修改内部数据。它将借用检查从编译期移到了运行期。
- 使用场景: Mock 对象,或者那些用单一可变性很难表达的复杂状态。
Drop:Rust 版本的 IDisposable
在 C# 中,你使用 using 和 IDisposable 来清理资源。而在 Rust 中,你需要实现 Drop trait。关键区别在于:Drop 是自动触发的。
#![allow(unused)]
fn main() {
struct TempFile { path: String }
impl Drop for TempFile {
fn drop(&mut self) {
println!("正在删除 {}", self.path);
// 当 'temp' 离开作用域时,清理工作一定会被执行。
}
}
}
关键理解: 在 Rust 中你永远不会“忘记”释放资源。只要所有者消失了,资源就会被清理掉。
决策树:该选哪种智能指针?
| 需求 | 智能指针 |
|---|---|
| 堆分配 (单一所有权) | Box<T> |
| 共享所有权 (单线程) | Rc<T> |
| 共享所有权 (多线程) | Arc<T> |
通过 &T 修改内部 (单线程) | RefCell<T> |
通过 &T 修改内部 (多线程) | Mutex<T> 或 RwLock<T> |
练习:为场景选择智能指针
挑战: 针对一个可能被多个线程同时访问的共享配置对象,你会选择哪种指针?
答案: Arc<T>。如果该配置还需要动态更新,则应使用 Arc<Mutex<T>>。
要点: 始终从最简单的拥有所有权的普通类型开始。只有在编译器给出提示(或者架构确实需要)时,再逐步升级到智能指针。