Rust 智能指针
通过 Box、Rc、RefCell 和 Arc 超越基本引用。学习智能指针如何在 Rust 中提供堆分配、共享所有权和内部可变性。
中级 Rust 系列的第 13 篇文章。在这篇文章中,普通的引用(&T)只是内存中的一个借用视图,而智能指针(Smart Pointers)是拥有所指向数据的一种数据结构。它们像引用一样工作,但带有额外的元数据和功能 — 堆分配、共享所有权和内部可变性。
graph TD
A["智能指针"] --> B["Box<T><br/>堆分配<br/>单一所有者"]
A --> C["Rc<T><br/>引用计数<br/>单线程共享"]
A --> D["RefCell<T><br/>内部可变性<br/>运行时借用检查"]
A --> E["Arc<T><br/>原子引用计数<br/>线程安全共享"]
A --> F["Deref trait<br/>自定义 * 运算符"]
是什么让指针变得”智能”?#
普通的引用(&T)只是一个地址 — 它借用的是由他人拥有的数据。而智能指针则拥有它所指向的数据。当智能指针离开作用域时,数据会被自动丢弃,内存会被释放。
其中的关键是 Deref trait。它允许智能指针通过自定义 * 运算符的行为来表现得像普通引用。
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}rust实现了 Deref 后,你可以使用 *my_box 就像使用 *reference 一样,Rust 会在后台自动调用 deref()。
Box:将数据放在堆上#
Box<T> 是最简单的智能指针。它在堆上分配数据,并提供一个唯一的所有者。当 Box 被丢弃时,堆内存会被释放。
为什么使用 Box?#
- 递归类型:像链表或树节点这样的自包含类型需要间接引用来拥有已知的大小。
- 大数据传输:将大数据移动到堆上可以避免昂贵的栈复制。
- trait 对象:在一个公共接口后面存储不同类型的值。
使用 Box 的递归类型#
如果没有 Box,像链表这样的递归结构体将具有无限的大小:
// ❌ 这段代码无法编译 — List 具有无限大小
enum List {
Cons(i32, List),
Nil,
}
// ✅ Box 通过添加间接引用打破循环
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
// 1 -> 2 -> 3 -> Nil
}rustgraph LR
A["Cons(1, Box)"] --> B["Cons(2, Box)"]
B --> C["Cons(3, Box)"]
C --> D["Nil"]
用于 trait 对象的 Box#
Box<dyn Trait> 允许在一个接口后面存储不同类型的值:
trait Draw {
fn draw(&self);
}
struct Button;
struct Slider;
impl Draw for Button {
fn draw(&self) { println!("绘制一个按钮"); }
}
impl Draw for Slider {
fn draw(&self) { println!("绘制一个滑块"); }
}
fn main() {
let components: Vec<Box<dyn Draw>> = vec![
Box::new(Button),
Box::new(Slider),
];
for component in &components {
component.draw();
}
}rustRc:共享所有权(单线程)#
有时你需要多个所有者拥有相同的数据。Rc<T>(Reference Counted - 引用计数)在单线程场景中实现了这一点。
工作原理#
每次调用 Rc::clone(),引用计数就会增加。当一个克隆被丢弃时,计数就会减少。只有当计数达到 0 时,数据才会被释放。
use std::rc::Rc;
fn main() {
let data = Rc::new(String::from("你好,共享的世界!"));
println!("引用计数: {}", Rc::strong_count(&data)); // 1
{
let data2 = Rc::clone(&data); // 计数增加
println!("引用计数: {}", Rc::strong_count(&data)); // 2
println!("data2: {data2}");
} // data2 被丢弃,计数减少
println!("引用计数: {}", Rc::strong_count(&data)); // 1
println!("data: {data}");
}rust使用 Rc 的树结构#
Rc<T> 非常适合子节点需要引用父节点的树结构:
use std::cell::RefCell;
use std::rc::Rc;
struct Node {
value: i32,
children: Vec<Rc<Node>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
children: vec![],
});
let branch = Rc::new(Node {
value: 5,
children: vec![Rc::clone(&leaf)],
});
println!("leaf 的引用计数: {}", Rc::strong_count(&leaf)); // 2
}rust[!NOTE]
Rc<T>使用非原子引用计数,速度更快但不是线程安全的。对于多线程代码,请使用Arc<T>。
RefCell:内部可变性#
RefCell<T> 启用了内部可变性 — 即使你只有一个对 RefCell 自身的不可变引用,也能修改其中的数据。
运行时的借用检查器#
Rc<T> 跟踪所有者的数量,而 RefCell<T> 在运行时跟踪引用的数量。规则与编译时借用相同,但违反规则会导致运行时 panic 而不是编译错误。
use std::cell::RefCell;
fn main() {
let cell = RefCell::new(5);
{
let mut borrow = cell.borrow_mut(); // 可变借用
*borrow += 1;
println!("在块内: {borrow}");
} // borrow 在此处被丢弃
println!("在块后: {}", cell.borrow()); // 不可变借用
}rustRc + RefCell 的组合#
将 Rc<T> 和 RefCell<T> 结合使用,可以获得共享所有权和修改能力:
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let data = Rc::new(RefCell::new(vec![1, 2, 3]));
let data2 = Rc::clone(&data);
data2.borrow_mut().push(4);
println!("data: {:?}", data.borrow()); // [1, 2, 3, 4]
}rust[!WARNING]
RefCell<T>在运行时会 panic,如果你违反借用规则(例如,同时进行两次可变借用)。这用灵活性交换了编译时安全性 — 请谨慎使用。
Arc:线程安全的共享所有权#
Arc<T>(Atomically Reference Counted - 原子引用计数)是 Rc<T> 的多线程版本。它使用原子操作来安全地在线程间共享数据。
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec![1, 2, 3, 4, 5]);
let mut handles = vec![];
for _ in 0..4 {
let data = Arc::clone(&data);
let handle = thread::spawn(move || {
let sum: i32 = data.iter().sum();
println!("线程总和: {sum}");
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
}rustgraph TD
A["Arc<Vec>"] --> B["线程 1<br/>Arc::clone"]
A --> C["线程 2<br/>Arc::clone"]
A --> D["线程 3<br/>Arc::clone"]
A --> E["线程 4<br/>Arc::clone"]
[!TIP]
Arc<T>由于原子操作存在一定的性能开销。如果不需要线程安全性,Rc<T>会更快。
何时使用哪种智能指针?#
| 智能指针 | 使用场景 | 线程安全? |
|---|---|---|
Box<T> | 堆分配、递归类型、trait 对象 | ✅ 是的(单一所有者) |
Rc<T> | 多个所有者、单线程 | ❌ 不是 |
RefCell<T> | 内部可变性、运行时借用检查 | ❌ 不是 |
Arc<T> | 多个所有者、多线程 | ✅ 是的 |
Rc<RefCell<T>> | 共享 + 可变、单线程 | ❌ 不是 |
Arc<Mutex<T>> | 共享 + 可变、多线程 | ✅ 是的 |
通俗比喻(ELI5):图书馆图书管理系统#
把智能指针想象成管理图书馆图书的不同方式:
Box<T>就像借一本书 — 你是唯一的所有者。归还(drop)时,图书馆会收回图书。Rc<T>就像一本多个学习小组可以一起借阅的书。图书馆会跟踪借阅的小组数量。只有最后一个小组归还时,图书馆才会收回。RefCell<T>就像一本带有”只读”封面的书。你可以翻看它,但要在里面做笔记,你需要图书管理员(运行时检查器)的特别许可 — 而且一次只能有一个人写。Arc<T>就像Rc<T>但适用于大学城多校区共享同一本书的图书馆系统。原子计数器确保没有任何校区会丢失图书。
总结#
- 智能指针拥有其数据并实现
Deref来表现得像引用。 Box<T>提供堆分配,是递归类型和 trait 对象的基础。Rc<T>通过引用计数在单线程代码中实现多所有者。RefCell<T>通过运行时借用检查提供内部可变性。Arc<T>是适用于多线程代码的Rc<T>线程安全版本。- 将这些组合在一起(例如
Rc<RefCell<T>>或Arc<Mutex<T>>)为共享的、可变的状态管理提供了强大的模式。
在下一篇文章中,我们将探索Functional Rust — 闭包和迭代器 — 以及 Rust 的零成本抽象如何让你编写出富有表现力的高级代码,而不会牺牲性能。
参考资料#
- The Rust Programming Language - Chapter 15: Smart Pointers ↗
- The Rust Programming Language - Chapter 15.1: Box ↗
- The Rust Programming Language - Chapter 15.4: Rc ↗
- The Rust Programming Language - Chapter 15.5: RefCell ↗
- The Rust Programming Language - Chapter 16.3: Arc ↗
- Rust by Example - Box, Stack and Heap ↗