第 15 章:智能指针
本章目标:理解智能指针如何拥有堆数据、提供引用式访问、共享所有权,以及为何内部可变性和引用计数需要谨慎使用。
Box<T>:在堆上存储数据
Box<T> 在堆上分配值,并在离开作用域时释放。它适合大型值、需要固定大小表示的递归类型,或需要把值放在堆上的场景。
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));递归枚举若直接嵌套自身会导致大小无法确定;Box 提供已知大小的指针间接层。普通引用只借用数据,Box 则拥有其指向的值。单一所有者默认优先使用普通值或 Box,只有确实需要共享时才引入引用计数。
智能指针如何像普通引用一样使用
实现 Deref 的类型可像引用一样解引用,Rust 也会在适用时进行解引用强制转换。Box<T> 因而能自然地访问内部值。不要把强制转换理解为所有权转移:它主要是引用类型之间的便利转换。
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(value: T) -> Self { Self(value) }
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target { &self.0 }
}
let value = MyBox::new(String::from("Rust"));
assert_eq!(value.len(), 4);Deref 强制转换会在调用点自动插入所需的解引用次数,并可在某些场景把 &String 转为 &str。实现 DerefMut 后还可支持可变目标转换;只有语义确实像透明指针时才实现这些 Trait,否则自动强制转换会让代码行为难以追踪。
用 Drop Trait 在清理时运行代码
Drop Trait 允许类型在值离开作用域时执行清理逻辑。Rust 按作用域结束顺序清理局部变量,通常是后创建者先清理。std::mem::drop(value) 可提前结束值的生命周期;不能直接调用 value.drop(),因为析构由语言负责调度。
自定义 Drop 可用于管理值以外的资源,但文件、锁和网络连接等标准库类型通常已经实现了清理逻辑。需要自定义资源类型时,应确保析构不 panic,并避免依赖不明确的析构顺序。
Rc<T>:引用计数智能指针
当多个部分需要共同拥有同一份不可变数据时,Rc<T> 使用引用计数管理生命周期。克隆 Rc 会增加计数而不是复制底层数据:
use std::rc::Rc;
let value = Rc::new(String::from("shared"));
let another = Rc::clone(&value);
assert_eq!(Rc::strong_count(&value), 2);
drop(another);
assert_eq!(Rc::strong_count(&value), 1);最后一个强引用离开作用域时,数据才会被释放。Rc 不是线程安全的;跨线程共享应考虑 Arc<T>。Rc<T> 本身只提供共享所有权,不能直接修改内部值。链表或图等多个节点共同拥有同一节点的结构,是它比普通所有权更合适的场景。
RefCell<T> 与内部可变性
RefCell<T> 在运行时检查借用规则,允许通过不可变外层绑定修改内部值。borrow() 返回 Ref<T>,borrow_mut() 返回 RefMut<T>;借用守卫离开作用域时,相应借用结束。若运行时违反“多个共享借用或一个可变借用”的规则,会 panic。它适合编译期无法表达、但程序逻辑能保证的单线程场景,不能替代多线程同步原语。
测试替身可以实现生产代码要求的 Trait,并把调用记录写入 RefCell<Vec<_>>。例如 Messenger Trait 的测试实现可在 send 时保存消息,再断言发送次数和内容;测试不必连接真实邮件或消息服务。这体现内部可变性的用途,也提醒我们冲突借用风险从编译期移到了运行时。
Rc<RefCell<T>> 组合共享所有权和内部可变性:多个所有者共享同一个 RefCell,并能在运行时修改其中的数据。这个组合灵活但更难推理,应缩小可变状态范围并避免长期保存借用守卫。
引用环会造成内存泄漏
若两个 Rc 值互相持有对方,强引用计数就永远不会归零,内存无法释放。Rust 保证内存安全,但不会自动检测或回收这种逻辑上的引用环。
Rc::downgrade 创建 Weak<T>,不增加强引用计数。使用前要调用 upgrade,它返回 Option<Rc<T>>,因为被引用值可能已经释放。树结构常让父节点通过 Rc 拥有子节点、子节点通过 Weak 指回父节点:拥有方向用强引用,观察方向用弱引用。
use std::{cell::RefCell, rc::{Rc, Weak}};
struct Node {
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
let leaf = Rc::new(Node {
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
let branch = Rc::new(Node {
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
assert!(leaf.parent.borrow().upgrade().is_some());Weak 的 upgrade 返回 None 是正常状态,调用方必须处理。设计共享图结构时,先画清楚谁拥有谁,再决定强引用和弱引用方向。
小结与练习
Box<T>提供堆分配,Rc<T>共享单线程所有权,RefCell<T>将借用检查移到运行时。Deref与Drop支持引用式访问和清理;Weak<T>可打断强引用环。- 练习:观察
Rc强引用计数在克隆和离开作用域后的变化;使用父子节点实现Rc/Weak关系,并检查父节点释放后upgrade的结果。
再练习:实现一个带 Drop 输出的小类型以观察清理顺序;用 MyBox<T> 实现 Deref;实现一个使用 RefCell 记录 Trait 调用的测试替身。
下一章:无畏并发。
官方对应章节:Smart Pointers。