智能指针
问题
Rust 有哪些智能指针?各自适用什么场景?
答案
智能指针总览
| 指针 | 所有权 | 可变性 | 线程安全 | 常用场景 |
|---|---|---|---|---|
Box<T> | 独占 | 遵循借用规则 | Send+Sync | 堆分配、递归类型、Trait Object |
Rc<T> | 共享 | 不可变 | ❌ | 单线程共享所有权 |
Arc<T> | 共享 | 不可变 | ✅ | 多线程共享所有权 |
Cell<T> | — | 内部可变 | ❌ | Copy 类型的内部可变性 |
RefCell<T> | — | 内部可变 | ❌ | 运行时借用检查 |
Mutex<T> | — | 内部可变 | ✅ | 多线程互斥访问 |
RwLock<T> | — | 内部可变 | ✅ | 多线程读写锁 |
Box
// 1. 堆分配
let b = Box::new(5);
println!("{}", *b); // 解引用
// 2. 递归类型
enum List {
Cons(i32, Box<List>),
Nil,
}
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
// 3. Trait Object
let shape: Box<dyn Shape> = Box::new(Circle { radius: 1.0 });
Rc(Reference Counting)
use std::rc::Rc;
let a = Rc::new(vec![1, 2, 3]);
let b = Rc::clone(&a); // 引用计数 +1(不拷贝数据)
let c = Rc::clone(&a);
println!("引用计数: {}", Rc::strong_count(&a)); // 3
// 所有 Rc 被 drop 后数据被释放
RefCell(运行时借用检查)
use std::cell::RefCell;
let cell = RefCell::new(vec![1, 2, 3]);
// 运行时借用检查(非编译时)
{
let mut borrow = cell.borrow_mut(); // 可变借用
borrow.push(4);
}
{
let borrow = cell.borrow(); // 不可变借用
println!("{:?}", borrow);
}
// ❌ 运行时 panic:已有不可变借用时不能可变借用
// let r = cell.borrow();
// let w = cell.borrow_mut(); // panic!
Rc + RefCell 组合
use std::cell::RefCell;
use std::rc::Rc;
// 多所有者 + 可变性
let shared = Rc::new(RefCell::new(vec![]));
let a = Rc::clone(&shared);
let b = Rc::clone(&shared);
a.borrow_mut().push(1);
b.borrow_mut().push(2);
println!("{:?}", shared.borrow()); // [1, 2]
Cell
use std::cell::Cell;
// Cell 适用于 Copy 类型,通过 get/set 操作
let cell = Cell::new(42);
cell.set(100);
println!("{}", cell.get()); // 100
// 常用于结构体中的计数器
struct Counter {
count: Cell<u32>,
}
impl Counter {
fn increment(&self) { // 不需要 &mut self
self.count.set(self.count.get() + 1);
}
}
内部可变性对比
常见面试问题
Q1: Box<T> 和直接在栈上存储有什么区别?
答案:
Box 将数据分配在堆上,栈上只保存一个指针(8 字节)。适用于:
- 大数据避免栈溢出
- 递归类型(编译器需要确定大小)
- Trait Object(大小不确定)
- 转移所有权时避免大数据拷贝
Q2: 什么时候用 Rc,什么时候用 Arc?
答案:
- 单线程共享所有权 →
Rc(无原子操作开销) - 多线程共享所有权 →
Arc(原子引用计数)
不要在单线程中使用 Arc——原子操作虽然开销小但非零。
Q3: RefCell 和编译时借用检查有什么区别?
答案:
| 特性 | 编译时检查 | RefCell 运行时检查 |
|---|---|---|
| 检查时机 | 编译期 | 运行时 |
| 违规后果 | 编译错误 | panic |
| 性能 | 零开销 | 微小开销 |
| 灵活性 | 有时过于严格 | 更灵活 |
RefCell 用于编译器无法证明借用安全但你确信没问题的场景(如图数据结构、观察者模式)。
Q4: Rc 会造成内存泄漏吗?
答案:
会。Rc 的循环引用会导致引用计数永远不归零,数据永远不释放。使用 Weak 打破循环:
use std::rc::{Rc, Weak};
struct Node {
parent: RefCell<Weak<Node>>, // 弱引用,不增加计数
children: RefCell<Vec<Rc<Node>>>, // 强引用
}
Q5: 智能指针如何实现 Deref?
答案:
所有智能指针都实现 Deref trait,让你可以像操作普通引用一样使用它们:
let b = Box::new(42);
println!("{}", *b); // Deref 解引用
println!("{}", b.abs()); // 自动 Deref 到 i32 的方法