Rust: Rust 智能指针:Box、Rc、RefCell 与内部可变性
最后更新:2026-08-26
智能指针(Smart Pointer)是"带额外行为的数据结构"——它像指针一样引用内存,但比普通指针多出自动内存管理、引用计数、借用检查等能力。
如果说普通指针是"一张写着地址的纸条",那智能指针就是"自带保安、清洁工和登记簿的物业管理系统"。Rust 的智能指针在编译时保证安全,没有运行时开销(除了引用计数的少量开销)。
1. 你将学到
Box<T>堆分配与所有权转移Dereftrait——解引用运算符重载Droptrait——析构函数自动清理Rc<T>引用计数——单线程多所有权共享RefCell<T>内部可变性——运行时借用检查Rc<RefCell<T>>组合模式——共享 + 可修改
2. 共享办公桌的故事
(1) 痛苦:多人想同时引用和修改同一份文档
周一早上,Luna 所在的公司推行"共享办公桌"政策。五个人的团队只有三张桌子。
- Alice 需要看一份项目计划书(PDF),但她不想自己打印一份——想看别人手上的那份
- Bob 也要看同一份计划书,而且他需要在上面做批注
- Carol 想同时看,但只读不改
- 问题是:如果 Alice 看完了把文档销毁了,Bob 和 Carol 还在看——这就会出问题
- 更糟的是,Bob 在改的时候 Alice 也在改——页面就乱了
"如果能知道有多少人还在看这份文档,等没人看了再销毁就好了……""如果能确保只有一个人在改的时候其他人不能改就好了……"
(2) Rust 智能指针的方案
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
// Box: 一个人独占一份文档,看完就销毁
let doc_box = Box::new(String::from("项目计划书 v1"));
println!("Box 持有: {}", doc_box);
// doc_box 离开作用域后自动销毁——不需要手动 free
// Rc: 多人共享只读访问
let doc_rc = Rc::new(String::from("共享计划书"));
let alice = Rc::clone(&doc_rc);
let bob = Rc::clone(&doc_rc);
println!("引用计数: {}", Rc::strong_count(&doc_rc)); // 3
// Rc<RefCell<T>>: 共享 + 可变
let doc_shared = Rc::new(RefCell::new(String::from("协作文档")));
let alice_view = Rc::clone(&doc_shared);
let bob_view = Rc::clone(&doc_shared);
// Bob 在文档上做批注(需要修改)
bob_view.borrow_mut().push_str("\nBob 的批注:第二部分需要补充");
// Alice 看文档(只读)
println!("Alice 看到: {}", alice_view.borrow());
// Carol 也看文档(只读)
println!("Carol 看到: {}", doc_shared.borrow());
}
Rust 用三种智能指针解决了"共享"和"修改"的线程安全问题:
Box独占所有权和堆分配、Rc共享只读所有权、RefCell在运行时检查借用规则实现内部可变性。
3. 核心概念
(1) 智能指针体系
graph TB
A[Rust 智能指针] --> B[Box<T> 堆分配]
A --> C[Rc<T> 引用计数]
A --> D[RefCell<T> 内部可变性]
B --> B1["let b = Box::new(42)"]
B --> B2["离开作用域自动 drop"]
C --> C1["Rc::clone 增加引用计数"]
C --> C2["strong_count == 0 时销毁"]
C --> C3["单线程环境使用"]
D --> D1["borrow() → 不可变引用"]
D --> D2["borrow_mut() → 可变引用"]
D --> D3["运行时检查借用规则"]
A --> E[组合模式]
E --> E1["Rc<RefCell<T>>"]
E --> E2["共享所有权 + 内部可变性"]
A --> F[核心 Trait]
F --> F1["Deref: 解引用运算符 *"]
F --> F2["Drop: 析构函数"]
(2) 三种智能指针对比
| 特性 | Box<T> | Rc<T> | RefCell<T> |
|---|---|---|---|
| 所有权 | 单一所有权 | 共享所有权(引用计数) | 单一所有权 |
| 可变性 | 可变(Box::new 后 *b = val) |
不可变(只读共享) | 内部可变(运行时检查) |
| 借用检查时机 | 编译时 | 编译时 | 运行时 |
| 性能开销 | 无额外开销 | 引用计数增减(少量) | 运行时借用检查(少量) |
| 线程安全 | 是 | 否(单线程) | 否(单线程) |
| 适用场景 | 堆分配大数据、递归类型 | 多路径共享只读数据 | 需要在不可变引用下修改 |
(3) Deref 与 Drop trait
| Trait | 方法 | 作用 |
|---|---|---|
Deref |
fn deref(&self) -> &T |
让 *x 解引用操作可以作用于自定义类型 |
Drop |
fn drop(&mut self) |
值离开作用域时自动调用的清理逻辑 |
(4) Box / Rc / Arc 选型对比
| 特性 | Box<T> |
Rc<T> |
Arc<T> |
|---|---|---|---|
| 所有权 | 单一所有者 | 多所有者(引用计数) | 多所有者(原子引用计数) |
| 线程安全 | 否(仅单线程) | 否(仅单线程) | 是(可跨线程共享) |
| 引用计数 | 无 | 非原子,开销低 | 原子操作,开销略高 |
| 可变访问 | &mut 直接可变 |
需 RefCell 配合 |
需 Mutex / RwLock 配合 |
| 典型场景 | 递归类型 / 大数据堆分配 | DAG / 共享只读数据 | 多线程共享数据 |
| 性能 | 最快 | 中等 | 稍慢(原子操作) |
Deref让智能指针用起来像普通引用——Box<T>可以像&T一样使用。Drop保证了资源自动释放——不需要手动free或delete。
4. 智能指针示例
▶ 示例 1:Box——堆分配与递归类型(难度 ⭐)
// ============================================
// Box<T> 的三种典型用法:堆分配、递归类型、Deref
// ============================================
// 递归类型:Cons list(必须用 Box,因为编译时大小未知)
#[derive(Debug)]
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
// 自定义智能指针(演示 Deref 和 Drop)
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0 // 返回内部值的引用
}
}
impl<T> Drop for MyBox<T> {
fn drop(&mut self) {
// 实际应用中在这里释放资源(如关闭文件、释放网络连接)
// println!("MyBox 被销毁了~");
}
}
fn main() {
// --- 1. Box 基本堆分配 ---
let b = Box::new(42);
println!("Box 中的值: {}", b); // 自动解引用
// --- 2. 递归类型:Cons List ---
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
println!("递归列表: {:?}", list);
// --- 3. 自定义 MyBox + Deref ---
let my_box = MyBox::new(String::from("Hello"));
// Deref 让 &MyBox<String> 自动变成 &String,再自动变成 &str
greet(&my_box);
// --- 4. 解引用运算符 ---
let x = 10;
let y = MyBox::new(x);
assert_eq!(10, *y); // *y 等价于 *(y.deref())
println!("*y == {}", *y);
}
fn greet(name: &str) {
println!("Greeting: {}", name);
}
输出:
Box 中的值: 42
递归列表: Cons(1, Cons(2, Cons(3, Nil)))
Greeting: Hello
*y == 10
Box<T>的核心价值:一是把数据放到堆上(栈上只保留指针),二是让编译时大小未知的类型(如递归类型)可以正常使用。Dereftrait 让Box<T>可以像&T一样被解引用——这就是"智能指针"的"智能"所在。
▶ 示例 2:Rc——引用计数共享所有权(难度 ⭐⭐)
// ============================================
// Rc<T> 引用计数:多路径共享只读数据
// ============================================
use std::rc::Rc;
#[derive(Debug)]
enum BookList {
Cons(String, Rc<BookList>),
Nil,
}
use BookList::{Cons, Nil};
fn main() {
// --- 场景:三本书被两个读者共享 ---
// 创建基础书单
let book_c = Rc::new(Cons("Rust 编程".to_string(),
Rc::new(Cons("算法导论".to_string(),
Rc::new(Cons("设计模式".to_string(),
Rc::new(Nil))))));
println!("初始引用计数: {}", Rc::strong_count(&book_c));
// Alice 借走了书单(Rc::clone 只增加引用计数,不深拷贝数据)
let alice = Rc::clone(&book_c);
println!("Alice 借走后引用计数: {}", Rc::strong_count(&book_c));
{
// Bob 也借走了书单(在内部作用域中)
let bob = Rc::clone(&book_c);
println!("Bob 借走后引用计数: {}", Rc::strong_count(&book_c));
// 两个人都可以读
println!("Alice 的书单: {:?}", *alice);
println!("Bob 的书单: {:?}", *bob);
} // Bob 离开作用域,引用计数减少
println!("Bob 还书后引用计数: {}", Rc::strong_count(&book_c));
// Alice 还在读
println!("Alice 还在读: {:?}", *alice);
// 所有 Rc 都离开作用域后,数据才真正被销毁
// Rc::strong_count == 0 时触发 drop
}
输出:
初始引用计数: 1
Alice 借走后引用计数: 2
Bob 借走后引用计数: 3
Alice 的书单: Cons("Rust 编程", Cons("算法导论", Cons("设计模式", Nil)))
Bob 的书单: Cons("Rust 编程", Cons("算法导论", Cons("设计模式", Nil)))
Bob 还书后引用计数: 2
Alice 还在读: Cons("Rust 编程", Cons("算法导论", Cons("设计模式", Nil)))
Rc<T>是"引用计数"(Reference Counted)的缩写。Rc::clone(&x)并不深拷贝数据,只是把引用计数加 1。当所有Rc句柄都离开作用域(引用计数归零)时,数据才被销毁。Rc只能用于单线程——多线程要用Arc<T>。
▶ 示例 3:RefCell——内部可变性(难度 ⭐⭐)
// ============================================
// RefCell<T>:运行时借用检查 + 内部可变性
// ============================================
use std::cell::RefCell;
// 模拟一个"记录日志"的 Messenger
// 外部只能拿到不可变引用,但内部需要修改日志
trait Messenger {
fn send(&self, msg: &str);
}
// 日志记录器:内部使用 RefCell 存储消息
struct Logger {
// 即使 Logger 本身不可变,messages 仍然可以修改
messages: RefCell<Vec<String>>,
}
impl Logger {
fn new() -> Logger {
Logger {
messages: RefCell::new(Vec::new()),
}
}
}
impl Messenger for Logger {
fn send(&self, msg: &str) {
// borrow_mut() 获取可变引用——即使 self 是 &self
self.messages.borrow_mut().push(msg.to_string());
}
}
fn main() {
let logger = Logger::new();
// 通过不可变引用调用 send——但内部实际上修改了
logger.send("用户登录");
logger.send("点击按钮");
logger.send("数据提交成功");
// borrow() 获取不可变引用
let msgs = logger.messages.borrow();
for (i, msg) in msgs.iter().enumerate() {
println!("日志 #{}: {}", i + 1, msg);
}
// --- 运行时借用违反示例(取消注释会 panic)---
// let mut_ref = logger.messages.borrow_mut();
// let ref1 = logger.messages.borrow(); // panic! 同时有可变和不可变引用
// println!("{}", ref1[0]);
println!("RefCell 演示完成");
}
输出:
日志 #1: 用户登录
日志 #2: 点击按钮
日志 #3: 数据提交成功
RefCell 演示完成
RefCell<T>的核心是"内部可变性"(Interior Mutability):即使Logger本身是&self不可变引用,RefCell仍然允许你修改内部数据。区别在于:编译时不检查借用规则,而是运行时检查——违反规则时程序会 panic,而不是编译报错。
▶ 示例 4:Rc<RefCell<T>>——共享 + 可变(难度 ⭐⭐⭐)
// ============================================
// Rc<RefCell<T>> 组合模式:共享所有权 + 内部可变性
// ============================================
use std::rc::Rc;
use std::cell::RefCell;
// 模拟一个"共享白板"——团队成员可以写和读
#[derive(Debug)]
struct Whiteboard {
content: RefCell<String>,
}
impl Whiteboard {
fn new() -> Whiteboard {
Whiteboard {
content: RefCell::new(String::new()),
}
}
fn write(&self, text: &str) {
let mut content = self.content.borrow_mut();
content.push_str(text);
content.push('\n');
}
fn read(&self) -> String {
self.content.borrow().clone()
}
}
fn main() {
// 创建一个共享白板,用 Rc 包裹使其可以被多人持有
let board = Rc::new(Whiteboard::new());
// Alice 和 Bob 都持有白板的引用
let alice_board = Rc::clone(&board);
let bob_board = Rc::clone(&board);
let carol_board = Rc::clone(&board);
println!("当前引用计数: {}", Rc::strong_count(&board)); // 4
// Alice 在白板上写
alice_board.write("Alice: 今天讨论 Rust 智能指针");
// Bob 补充内容
bob_board.write("Bob: 我来讲 Box 和 Rc 的区别");
// Carol 也写
carol_board.write("Carol: RefCell 的运行时借用检查很重要");
// 所有人都能看到完整内容(因为共享同一个 RefCell)
println!("=== 白板内容 ===");
println!("{}", board.read());
// 验证所有引用都指向同一份数据
println!("Alice 看到的内容长度: {}", alice_board.read().len());
println!("Bob 看到的内容长度: {}", bob_board.read().len());
println!("Carol 看到的内容长度: {}", carol_board.read().len());
// board 离开作用域时,引用计数归零,Whiteboard 自动销毁
}
输出:
当前引用计数: 4
=== 白板内容 ===
Alice: 今天讨论 Rust 智能指针
Bob: 我来讲 Box 和 Rc 的区别
Carol: RefCell 的运行时借用检查很重要
Alice 看到的内容长度: 73
Bob 看到的内容长度: 73
Carol 看到的内容长度: 73
Rc<RefCell<T>>是 Rust 单线程编程中最强大的组合模式之一:Rc解决"多个人都想持有"的问题,RefCell解决"持有不可变引用的人也想修改"的问题。如果把Rc比作"共享借阅卡",那RefCell就是"允许批注的特殊副本"。
▶ 示例 5:综合练习——图数据结构(难度 ⭐⭐⭐)
// ============================================
// 综合示例:Rc<RefCell<T>> 实现图节点
// ============================================
use std::rc::{Rc, Weak};
use std::cell::RefCell;
use std::fmt;
struct Node {
value: String,
neighbors: Vec<Weak<RefCell<Node>>>,
}
impl Node {
fn new(value: &str) -> Rc<RefCell<Node>> {
Rc::new(RefCell::new(Node {
value: value.to_string(),
neighbors: Vec::new(),
}))
}
fn add_neighbor(node: &Rc<RefCell<Node>>, neighbor: &Rc<RefCell<Node>>) {
node.borrow_mut().neighbors.push(Rc::downgrade(neighbor));
}
}
impl fmt::Debug for Node {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let neighbor_names: Vec<String> = self.neighbors.iter()
.filter_map(|w| w.upgrade())
.map(|n| n.borrow().value.clone())
.collect();
write!(f, "{} -> {:?}", self.value, neighbor_names)
}
}
fn main() {
let a = Node::new("A");
let b = Node::new("B");
let c = Node::new("C");
let d = Node::new("D");
Node::add_neighbor(&a, &b);
Node::add_neighbor(&a, &c);
Node::add_neighbor(&b, &c);
Node::add_neighbor(&b, &d);
Node::add_neighbor(&c, &d);
println!("=== 图结构 ===");
for node in [&a, &b, &c, &d] {
println!("{:?}", node.borrow());
}
println!("\n=== 从 A 出发可达的节点 ===");
if let Some(neighbor) = a.borrow().neighbors.first().and_then(|w| w.upgrade()) {
println!("A 的第一个邻居: {}", neighbor.borrow().value);
let second_hop: Vec<String> = neighbor.borrow().neighbors.iter()
.filter_map(|w| w.upgrade())
.map(|n| n.borrow().value.clone())
.collect();
println!("从该邻居再走一步: {:?}", second_hop);
}
println!("\n=== 强引用计数 ===");
println!("A 的 Rc 引用数: {}", Rc::strong_count(&a));
let a_clone = Rc::clone(&a);
println!("clone 后 A 的 Rc 引用数: {}", Rc::strong_count(&a));
drop(a_clone);
println!("drop 后 A 的 Rc 引用数: {}", Rc::strong_count(&a));
}
输出:
=== 图结构 ===
A -> ["B", "C"]
B -> ["C", "D"]
C -> ["D"]
D -> []
=== 从 A 出发可达的节点 ===
A 的第一个邻居: B
从该邻居再走一步: ["C", "D"]
=== 强引用计数 ===
A 的 Rc 引用数: 1
clone 后 A 的 Rc 引用数: 2
drop 后 A 的 Rc 引用数: 1
图节点用
Rc<RefCell<Node>>实现共享所有权 + 内部可变性;邻居关系用Weak<RefCell<Node>>避免循环引用导致内存泄漏。Rc::downgrade创建弱引用,weak.upgrade()尝试升级为强引用。
❓ 常见问题
Box<T> 和普通引用 &T 有什么区别?Box<T> 拥有数据的所有权,&T 只是借用。Rc<T> 和 Arc<T> 有什么区别?Rc<T> 是单线程引用计数,Arc<T> 是多线程原子引用计数。RefCell<T> 和 Cell<T> 有什么区别?Cell<T> 通过值拷贝(或移动)实现内部可变性,RefCell<T> 通过引用实现。RefCell<T> 而不是 &mut T?RefCell。Rc<RefCell<T>> 和 &mut T 在性能上有什么区别?Rc<RefCell<T>> 有引用计数和运行时借用检查的开销,&mut T 是零开销。📖 小节
Box<T>是堆分配智能指针,用于递归类型、大数据和 trait 对象——所有权唯一,零额外开销Dereftrait 让智能指针支持*解引用运算符,Droptrait 保证离开作用域时自动清理资源Rc<T>是引用计数智能指针,实现单线程下的多所有权共享——Rc::clone只增加计数,不拷贝数据RefCell<T>提供内部可变性,将借用检查从编译时推迟到运行时——违反规则会 panicCell<T>通过值拷贝实现内部可变性(要求T: Copy),RefCell<T>通过引用实现(不限类型)Rc<RefCell<T>>组合模式 = 共享所有权 + 内部可变性,是 Rust 单线程编程中最常用的灵活数据共享方案
📝 作业
- 难度 ⭐:写一个程序,使用
Box<T>创建一个递归的Cons List(包含至少 3 个元素),然后打印整个列表。要求使用#[derive(Debug)]打印。 - 难度 ⭐⭐:实现一个"共享笔记"系统:使用
Rc<RefCell<String>>让三个人(Alice、Bob、Carol)可以共享一个笔记字符串。每个人都能读,每个人都能写(追加内容)。验证所有人在写完后都能看到完整内容。 - 难度 ⭐⭐⭐:实现一个"图形观察者"模式。定义
trait Observer { fn notify(&self, msg: &str); },然后实现一个LoggerObserver(内部用RefCell<Vec<String>>记录消息)。再用Rc<RefCell<dyn Observer>>让多个观察者可以被同一个 subject 通知。在 main 中创建两个观察者,subject 发送三条消息,验证两个观察者都收到了所有消息。