智能指针
一句话理解
所有权规则是编译期的严格约束。当”一个值只有一个所有者”或”不可变就是不可变”满足不了需求时,用智能指针把一部分检查挪到运行时或挪到原子操作上:
Box<T>:唯一所有权 + 堆分配Rc<T>/Arc<T>:多个所有者(共享)RefCell<T>/Mutex<T>:内部可变性Weak<T>:不打引用计数的”弱”引用,用来打破循环
1. Box<T>:最简单的堆分配
let b = Box::new(5);
println!("{}", b); // 自动解引用三个典型用途:
① 递归类型必须装箱(否则大小无法确定)
enum List {
Cons(i32, Box<List>),
Nil,
}② 把大对象从栈挪到堆(避免拷贝、稳定地址)
③ 装 trait 对象
let handlers: Vec<Box<dyn Fn(i32) -> i32>> = vec![
Box::new(|x| x + 1),
Box::new(|x| x * 2),
];
Box不是"引用"
Box<T>拥有T,离开作用域会 drop 它。它只是把数据放在堆上,语义上仍是唯一所有者。
2. Rc<T> 与 Arc<T>:共享所有权
use std::rc::Rc;
let a = Rc::new(String::from("共享数据"));
let b = Rc::clone(&a); // 引用计数 +1,不拷贝数据
println!("{}", Rc::strong_count(&a)); // 2Rc<T> | Arc<T> | |
|---|---|---|
| 计数方式 | 普通整数加减 | 原子操作 |
| 线程安全 | ❌ !Send !Sync | ✅ Send + Sync(当 T: Send + Sync) |
| 开销 | 更小更快 | 有原子操作成本 |
| 用在哪 | 单线程共享 | 跨线程共享 |
别用
Arc当默认选择
Arc的原子操作不是免费的。只在确实要跨线程时用Arc,单线程内一律Rc。
Rc::clone(&a) 语义上不是”克隆数据”,只是把计数 +1。写 a.clone() 会让读者误以为在深拷贝,圈内惯例是显式写 Rc::clone(&a)。
3. 内部可变性:RefCell 与 Cell
Rc<T> 只给你不可变访问。想要”共享 + 可改”,需要内部可变性:
use std::cell::RefCell;
let c = RefCell::new(5);
*c.borrow_mut() += 1;
println!("{}", c.borrow()); // 6RefCell 把借用检查从编译期挪到运行时:
borrow()/borrow_mut()返回智能指针,drop 时释放借用- 违反规则(同时存在
borrow与borrow_mut)会panic,而不是编译错误 - 因为运行时记账,
RefCell不是Sync——不能跨线程共享
Cell<T> 更轻,但只适合 Copy 类型或整体替换:
use std::cell::Cell;
let c = Cell::new(1);
c.set(c.get() + 1);Cell<T> | RefCell<T> | |
|---|---|---|
| 获取引用 | ❌ 只能 get/set 整体读写 | ✅ borrow/borrow_mut |
| 适用 | Copy 类型的小状态 | 需要借用的复杂结构 |
| 违规行为 | 不存在(无借用) | 运行时 panic |
4. 跨线程的内部可变性:Mutex 与 RwLock
use std::sync::{Arc, Mutex};
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let c = Arc::clone(&counter);
handles.push(std::thread::spawn(move || {
let mut n = c.lock().unwrap(); // 拿锁,返回 MutexGuard
*n += 1;
})); // guard 在这里 drop,自动解锁
}
for h in handles { h.join().unwrap(); }
println!("{}", *counter.lock().unwrap()); // 10Mutex<T> | RwLock<T> | |
|---|---|---|
| 并发读 | ❌ 独占 | ✅ 多个读者 |
| 写 | ✅ | ✅ 独占 |
| 适合 | 读写都频繁、临界区短 | 读多写少 |
| 注意 | 可能死锁;.lock() 返回 Result(锁中毒) | 写者可能饥饿;实现依赖平台 |
Mutex的锁中毒持锁线程 panic 时锁会被标记为 “poisoned”,后续
.lock()返回Err。很多代码直接.unwrap(),等于把上一个线程的 panic 传染给所有线程。生产代码里可以考虑PoisonError::into_inner()显式忽略,或统一在任务边界处理 panic。
5. 两个经典组合
Rc<RefCell<T>>:单线程共享可变状态
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Node {
value: i32,
next: Option<Rc<RefCell<Node>>>,
}
let a = Rc::new(RefCell::new(Node { value: 1, next: None }));
let b = Rc::new(RefCell::new(Node { value: 2, next: Some(Rc::clone(&a)) }));Arc<Mutex<T>>:跨线程共享可变状态
见上一节的计数器例子。这是 Rust 里最标准的”共享可变状态”配方:
Arc 负责共享所有权(跨线程)
Mutex 负责互斥访问(内部可变性)先想想能不能不用共享状态
消息传递(
channel)+ 单一所有者,往往比Arc<Mutex<_>>更容易推理、也更少死锁。Rust 社区的说法是”用通信来共享内存,而不是共享内存来通信”。共享状态是手段,不是默认。
6. Weak<T>:打破循环引用
Rc / Arc 的引用计数不会自动发现环,环会让计数永远不为零,导致内存泄漏——Rust 只保证不悬垂,不保证不泄漏。
use std::rc::{Rc, Weak};
use std::cell::RefCell;
struct Node {
parent: RefCell<Weak<Node>>, // 父节点用弱引用,避免环
children: RefCell<Vec<Rc<Node>>>,
}
let leaf = Rc::new(Node {
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
let branch = Rc::new(Node {
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);Weak::upgrade() 返回 Option<Rc<T>>:对象还活着就是 Some,已被回收就是 None。
典型需要弱引用的地方:父子双向关系、观察者/订阅者列表、缓存(键的引用不该阻止条目被回收)。
7. Cow<'a, T>:写时克隆
use std::borrow::Cow;
fn normalize(s: &str) -> Cow<'_, str> {
if s.contains(' ') {
Cow::Owned(s.replace(' ', "_")) // 需要改才分配
} else {
Cow::Borrowed(s) // 不改就不分配
}
}Cow(Clone-on-Write)只有两种状态:借用或拥有。适合”大多数输入不需要修改”的场景,能省掉大量不必要的分配。
8. 怎么选:一张决策表
| 需求 | 选择 |
|---|---|
| 唯一所有权,但要在堆上 / 递归类型 / trait object | Box<T> |
| 多个所有者,单线程 | Rc<T> |
| 多个所有者,跨线程 | Arc<T> |
| 单线程 + 需要修改共享值 | Rc<RefCell<T>> |
| 跨线程 + 需要修改共享值 | Arc<Mutex<T>> / Arc<RwLock<T>> |
| 共享但允许”有时改” | Cow<'a, T> |
| 打破循环引用 | Weak<T> |
只需在少数地方改值,且类型是 Copy | Cell<T> |
9. 与所有权规则的关系
一句话看穿
智能指针并没有绕过所有权,而是换了一种执行方式:
Rc/Arc把”唯一所有权”放宽成”多个所有者 + 计数”,代价是运行时计数和可能的泄漏RefCell/Mutex把”借用规则”从编译期挪到运行时,代价是 panic 风险或加锁开销Box完全遵守原来的规则,只是加了一层堆间接也就是说:越灵活,越多运行时风险。能用编译期检查解决,就不要用智能指针。