Rust 智能指针详解
先搞清楚一件事
Rust 的”智能指针”概念和 C++ 完全是两码事。在 C++ 里,智能指针是用来擦屁股的——程序员管不好内存,标准库帮你想办法。Rust 不一样,所有权系统在编译期就解决了 99% 的内存安全问题,所谓的智能指针只是所有权规则的延伸。
记住这句话:Rust 的智能指针不是为了修复烂代码,而是为了实现特定的数据结构需求。
Box:最简单的堆分配
Box<T> 就是 Rust 版的 std::unique_ptr,但更简单、更严格。
fn main() {
// 在堆上分配一个 i32
let b = Box::new(5);
println!("{}", b); // 自动解引用
// Box 离开作用域,内存自动释放
} // drop 在这里调用什么时候用 Box?
- 递归类型必须用它(这是编译器逼你的):
struct ListNode {
val: i32,
next: Option<Box<ListNode>>
}- 数据太大,不想栈上拷贝:
fn process(data: Box<[u8; 1024 * 1024]>) {
// 转移所有权,零拷贝
}- Trait Object:
fn factory() -> Box<dyn Draw> {
Box::new(Circle::new())
}Rc:单线程共享所有权
Rc<T>(Reference Counted)是单线程的共享指针。注意:它只适用于单线程。
use std::rc::Rc;
fn main() {
let data = Rc::new(vec![1, 2, 3]);
// clone 只增加引用计数,不拷贝数据
let data2 = Rc::clone(&data);
let data3 = Rc::clone(&data);
println!("引用计数: {}", Rc::strong_count(&data)); // 3
// 所有 Rc 都离开作用域,数据才真正释放
}核心特点:
- 不可变借用:你只能读,不能写
- 非线程安全:没有
Send和Synctrait - 有循环引用风险(需要用
Weak<T>破解)
use std::rc::{Rc, Weak};
use std::cell::RefCell;
struct Node {
value: i32,
parent: RefCell<Weak<Node>>, // Weak 打破循环
children: RefCell<Vec<Rc<Node>>>, // Rc 拥有子节点
}Arc:线程安全的共享
Arc<T>(Atomic Reference Counted)是线程安全版的 Rc<T>。内部用原子操作维护引用计数,有性能开销。
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec![1, 2, 3]);
let handles: Vec<_> = (0..10)
.map(|i| {
let data = Arc::clone(&data);
thread::spawn(move || {
println!("线程 {}: {:?}", i, data);
})
})
.collect();
for h in handles {
h.join().unwrap();
}
}Arc 的坑:
Arc 给你的是共享所有权,但如果你需要修改数据,还得配合 Mutex<T> 或 RwLock<T>:
use std::sync::{Arc, Mutex};
fn main() {
let counter = Arc::new(Mutex::new(0));
let handles: Vec<_> = (0..10)
.map(|_| {
let counter = Arc::clone(&counter);
thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
})
})
.collect();
for h in handles {
h.join().unwrap();
}
println!("结果: {}", *counter.lock().unwrap());
}RefCell:运行期借用检查
RefCell<T> 实现了内部可变性(Interior Mutability)——让你能在不可变引用里修改数据。
原理:
- 普通借用检查在编译期进行
- RefCell 把检查推迟到运行期
- 违反规则会panic,而不是编译错误
use std::cell::RefCell;
fn main() {
let cell = RefCell::new(5);
{
let mut v = cell.borrow_mut(); // 可变借用
*v += 1;
} // 借用在这里结束
let v = cell.borrow(); // 不可变借用
println!("{}", v); // 6
}双借用错误(运行期):
let cell = RefCell::new(5);
let _a = cell.borrow_mut();
let _b = cell.borrow_mut(); // thread 'main' panicked at already borrowedRc<RefCell
这是单线程下共享可变数据的经典套路:
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
let shared = Rc::new(RefCell::new(Vec::new()));
let s1 = Rc::clone(&shared);
let s2 = Rc::clone(&shared);
s1.borrow_mut().push(1);
s2.borrow_mut().push(2);
println!("{:?}", shared.borrow()); // [1, 2]
}Cell:更轻量的内部可变性
Cell<T> 也是内部可变性,但比 RefCell 更轻量。它只适用于实现了 Copy trait 的类型。
use std::cell::Cell;
fn main() {
let cell = Cell::new(5);
cell.set(10); // 直接设置新值
let old = cell.replace(20); // 替换并返回旧值
println!("old: {}, new: {}", old, cell.get()); // old: 10, new: 20
}Cell vs RefCell:
| 特性 | Cell | RefCell |
|---|---|---|
| 适用类型 | Copy 类型 | 任意类型 |
| 借用检查 | 无(整体替换) | 运行期检查 |
| 开销 | 极低 | 有一定开销 |
| 获取引用 | 不能 | borrow() / borrow_mut() |
与 C++ 智能指针的对比
| 特性 | Rust | C++ |
|---|---|---|
| 所有权检查 | 编译期强制 | 运行期/程序员自律 |
| 默认行为 | 移动语义 | 拷贝语义 |
Box<T> / unique_ptr | 编译期 guarantee | 运行期可能 nullptr |
Rc<T> / shared_ptr | 明确单线程限制 | 一律线程安全(有开销) |
| 循环引用 | Weak 明确标记 | 程序员自己注意 |
| 性能 | 零成本抽象 | 虚函数/控制块开销 |
Linus 的评价:
“Rust 这帮家伙做对了一件事——让编译器当坏人。
C++ 的智能指针是亡羊补牢,Rust 的所有权系统是防患于未然。
我讨厌 C++ 的复杂性,但 Rust 的编译错误虽然烦人,至少问题在编译期就暴露了。”
选择指南:实际场景
// 场景 1:树结构,子节点被父节点拥有
struct TreeNode {
value: i32,
children: Vec<Rc<RefCell<TreeNode>>>,
parent: Option<Weak<RefCell<TreeNode>>>,
}
// 场景 2:多线程共享配置
lazy_static! {
static ref CONFIG: Arc<RwLock<Config>> = Arc::new(RwLock::new(Config::default()));
}
// 场景 3:回调函数闭包捕获
let data = Rc::new(RefCell::new(Vec::new()));
let closure = {
let data = Rc::clone(&data);
move || {
data.borrow_mut().push(42);
}
};
// 场景 4:大型数据避免栈拷贝
fn process_big_data(data: Box<[u8; 1024 * 1024]>) {
// 数据已经在堆上,转移所有权即可
}常见的混合使用
实际项目中,智能指针经常组合使用:
// Rc<RefCell<T>> - 单线程共享可变数据
let state = Rc::new(RefCell::new(GameState::new()));
state.borrow_mut().score += 100;
// Arc<Mutex<T>> - 多线程共享可变数据
let counter = Arc::new(Mutex::new(0));
*counter.lock().unwrap() += 1;
// Arc<RwLock<T>> - 多读单写场景
let config = Arc::new(RwLock::new(Config::default()));
let cfg = config.read().unwrap(); // 多线程可同时读
let mut cfg = config.write().unwrap(); // 独占写
// Rc<Cell<T>> - 轻量级计数器
let count = Rc::new(Cell::new(0));
count.set(count.get() + 1);
// Rc<RefCell<Vec<T>>> - 共享可变集合
let callbacks: Rc<RefCell<Vec<Box<dyn Fn()>>>> =
Rc::new(RefCell::new(Vec::new()));
callbacks.borrow_mut().push(Box::new(|| println!("callback")));选择建议:
| 场景 | 组合 | 注意点 |
|---|---|---|
| 单线程 + 共享 + 可变 | Rc<RefCell<T>> | 运行期检查,可能 panic |
| 单线程 + Copy类型可变 | Rc<Cell<T>> | 无借用检查,更轻量 |
| 多线程 + 共享 + 可变 | Arc<Mutex<T>> | 锁粒度要小,避免死锁 |
| 多读少写 | Arc<RwLock<T>> | 注意写锁饥饿问题 |
| 递归数据结构 | Box + Rc + Weak | 用 Weak 打破循环引用 |
关键原则
- 默认用 ownership + borrowing,别一上来就用智能指针
- Box
:递归类型、大对象、Trait Object - Rc
:单线程共享只读数据(配合 RefCell 可写) - Arc
:多线程共享只读数据(配合 Mutex/RwLock 可写) - Cell
:Copy 类型的内部可变性 - RefCell
:非 Copy 类型的内部可变性(单线程)
记住: Rust 的智能指针是为了表达特定的所有权语义,不是为了修复你的设计缺陷。如果你在纠结用哪个指针,先想想你的所有权模型设计是否正确。
2. Box
3. Rc
4. Arc
5. Cell
6. RefCell

