先搞清楚一件事

Rust 的"智能指针"概念和 C++ 完全是两码事。在 C++ 里,智能指针是用来擦屁股的——程序员管不好内存,标准库帮你想办法。Rust 不一样,所有权系统在编译期就解决了 99% 的内存安全问题,所谓的智能指针只是所有权规则的延伸。

记住这句话:Rust 的智能指针不是为了修复烂代码,而是为了实现特定的数据结构需求。

Box:最简单的堆分配

Box<T> 就是 Rust 版的 std::unique_ptr,但更简单、更严格。

1
2
3
4
5
6
7
fn main() {
// 在堆上分配一个 i32
let b = Box::new(5);
println!("{}", b); // 自动解引用

// Box 离开作用域,内存自动释放
} // drop 在这里调用

什么时候用 Box?

  1. 递归类型必须用它(这是编译器逼你的):
1
2
3
4
struct ListNode {
val: i32,
next: Option<Box<ListNode>>
}
  1. 数据太大,不想栈上拷贝:
1
2
3
fn process(data: Box<[u8; 1024 * 1024]>) {
// 转移所有权,零拷贝
}
  1. Trait Object:
1
2
3
fn factory() -> Box<dyn Draw> {
Box::new(Circle::new())
}

Rc:单线程共享所有权

Rc<T>(Reference Counted)是单线程的共享指针。注意:它只适用于单线程

1
2
3
4
5
6
7
8
9
10
11
12
13
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 都离开作用域,数据才真正释放
}

核心特点:

  • 不可变借用:你只能读,不能写
  • 非线程安全:没有 SendSync trait
  • 有循环引用风险(需要用 Weak<T> 破解)
1
2
3
4
5
6
7
8
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>。内部用原子操作维护引用计数,有性能开销。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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,而不是编译错误
1
2
3
4
5
6
7
8
9
10
11
12
13
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
}

双借用错误(运行期):

1
2
3
let cell = RefCell::new(5);
let _a = cell.borrow_mut();
let _b = cell.borrow_mut(); // thread 'main' panicked at already borrowed

Rc<RefCell> 组合:

这是单线程下共享可变数据的经典套路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 的类型。

1
2
3
4
5
6
7
8
9
10
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:

特性CellRefCell
适用类型Copy 类型任意类型
借用检查无(整体替换)运行期检查
开销极低有一定开销
获取引用不能borrow() / borrow_mut()

与 C++ 智能指针的对比

特性RustC++
所有权检查编译期强制运行期/程序员自律
默认行为移动语义拷贝语义
Box<T> / unique_ptr编译期 guarantee运行期可能 nullptr
Rc<T> / shared_ptr明确单线程限制一律线程安全(有开销)
循环引用Weak 明确标记程序员自己注意
性能零成本抽象虚函数/控制块开销

Linus 的评价:

"Rust 这帮家伙做对了一件事——让编译器当坏人

C++ 的智能指针是亡羊补牢,Rust 的所有权系统是防患于未然。
我讨厌 C++ 的复杂性,但 Rust 的编译错误虽然烦人,至少问题在编译期就暴露了。"

选择指南:实际场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 场景 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]>) {
// 数据已经在堆上,转移所有权即可
}

常见的混合使用

实际项目中,智能指针经常组合使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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 打破循环引用

关键原则

  1. 默认用 ownership + borrowing,别一上来就用智能指针
  2. Box:递归类型、大对象、Trait Object
  3. Rc:单线程共享只读数据(配合 RefCell 可写)
  4. Arc:多线程共享只读数据(配合 Mutex/RwLock 可写)
  5. Cell:Copy 类型的内部可变性
  6. RefCell:非 Copy 类型的内部可变性(单线程)

记住: Rust 的智能指针是为了表达特定的所有权语义,不是为了修复你的设计缺陷。如果你在纠结用哪个指针,先想想你的所有权模型设计是否正确
2. Box:递归类型、大对象、Trait Object
3. Rc:单线程共享只读数据(配合 RefCell 可写)
4. Arc:多线程共享只读数据(配合 Mutex/RwLock 可写)
5. Cell:Copy 类型的内部可变性
6. RefCell:非 Copy 类型的内部可变性(单线程)