第十章:智能指针——超越普通引用
本章导读:引用是 Rust 内存安全的基石,但有时我们需要更多的能力:共享所有权、延迟处理、自定义内存布局。智能指针就是这样的存在——它们看起来像引用,但拥有额外的超能力。本章将探索 Rust 生态中最重要的智能指针,理解它们如何扩展所有权模型。
🎯 10.1 什么是智能指针?
智能指针是表现类似指针的数据结构,但拥有额外的元数据和功能。
🔍 10.1.1 引用 vs 智能指针
| 特性 | 引用 &T | 智能指针 |
|---|---|---|
| 所有权 | 借用 | 拥有 |
| 元数据 | 生命周期 | 可自定义 |
| 析构行为 | 无 | 可自定义 Drop |
| 开销 | 零 | 可能有 |
📦 10.1.2 智能指针的模式
智能指针通常实现 Deref 和 Drop trait:
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
// Deref:让智能指针像引用一样使用
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
// Drop:自定义析构行为
impl<T> Drop for MyBox<T> {
fn drop(&mut self) {
println!("MyBox 被释放了");
}
}
📦 10.2 Box<T>:堆分配
Box 是最简单的智能指针,将数据存储在堆上。
🏠 10.2.1 基本用法
fn main() {
// 在栈上存储指针,在堆上存储数据
let b = Box::new(5);
println!("b = {}", b); // 自动解引用
// 离开作用域时自动释放堆内存
}
🎯 10.2.2 使用场景
递归类型:解决类型大小未知的问题
// 编译失败:无限大小的类型
// 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))))));
fn print_list(list: &List) {
match list {
Cons(value, next) => {
print!("{} ", value);
print_list(next);
}
Nil => println!(),
}
}
print_list(&list); // 1 2 3
}
大型数据转移:避免栈拷贝
struct LargeArray([u8; 1000000]);
fn process(data: Box<LargeArray>) {
// 只移动指针,不拷贝数据
}
fn main() {
let data = Box::new(LargeArray([0u8; 1000000]));
process(data);
}
Trait 对象:存储不同具体类型
trait Animal {
fn speak(&self);
}
struct Dog;
impl Animal for Dog {
fn speak(&self) { println!("汪汪!"); }
}
struct Cat;
impl Animal for Cat {
fn speak(&self) { println!("喵喵!"); }
}
fn main() {
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog),
Box::new(Cat),
];
for animal in &animals {
animal.speak();
}
}
🔄 10.3 Rc<T>:共享所有权
Rc(Reference Counting)允许多个所有者共享同一数据。
📊 10.3.1 基本用法
use std::rc::Rc;
fn main() {
let data = Rc::new(vec![1, 2, 3]);
// 克隆增加引用计数
let a = Rc::clone(&data);
let b = Rc::clone(&data);
println!("引用计数: {}", Rc::strong_count(&data)); // 3
// 所有引用都指向同一数据
println!("a: {:?}", a);
println!("b: {:?}", b);
// 最后一个引用离开作用域时数据被释放
}
🌳 10.3.2 共享数据结构
use std::rc::Rc;
#[derive(Debug)]
struct Node {
value: i32,
children: Vec<Rc<Node>>,
}
fn main() {
let leaf = Rc::new(Node { value: 1, children: vec![] });
let branch = Rc::new(Node {
value: 2,
children: vec![Rc::clone(&leaf)], // leaf 被共享
});
// leaf 现在被两个地方引用
println!("leaf 引用计数: {}", Rc::strong_count(&leaf)); // 2
// 另一个分支也共享 leaf
let another_branch = Node {
value: 3,
children: vec![Rc::clone(&leaf)],
};
println!("leaf 引用计数: {}", Rc::strong_count(&leaf)); // 3
}
⚠️ 10.3.3 注意事项
Rc不是线程安全的,多线程环境使用ArcRc不可变,如需修改使用RefCell- 小心循环引用,会导致内存泄漏
🧵 10.4 Arc<T>:线程安全的共享所有权
Arc(Atomic Reference Counting)是 Rc 的线程安全版本。
🔒 10.4.1 基本用法
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..3 {
let data_clone = Arc::clone(&data);
handles.push(thread::spawn(move || {
println!("线程看到: {:?}", data_clone);
}));
}
for handle in handles {
handle.join().unwrap();
}
}
⚡ 10.4.2 性能考虑
Arc 使用原子操作,比 Rc 慢一些:
| 操作 | Rc | Arc |
|---|---|---|
| Clone | 普通加法 | 原子加法 |
| Drop | 普通减法 | 原子减法 |
| 访问 | 直接 | 直接 |
选择指南:单线程用
Rc,多线程用Arc。在性能关键的场景,避免频繁克隆Arc。
🔓 10.5 RefCell<T>:内部可变性
RefCell 允许在不可变引用的情况下修改内部数据。
🎭 10.5.1 内部可变性模式
use std::cell::RefCell;
fn main() {
let data = RefCell::new(5);
// data 是不可变的,但我们可以修改内部值
*data.borrow_mut() += 1;
println!("data = {}", data.borrow());
}
📖 10.5.2 借用规则:运行时检查
与引用的编译时检查不同,RefCell 在运行时检查借用规则:
use std::cell::RefCell;
fn main() {
let data = RefCell::new(5);
// 正确:多个不可变借用
{
let r1 = data.borrow();
let r2 = data.borrow();
println!("{} {}", r1, r2);
}
// 正确:单个可变借用
{
let mut r = data.borrow_mut();
*r += 1;
}
// 运行时 panic:同时存在可变和不可变借用
// let r1 = data.borrow();
// let mut r2 = data.borrow_mut(); // panic!
}
🎯 10.5.3 使用场景
use std::cell::RefCell;
struct Messenger {
messages: RefCell<Vec<String>>,
}
impl Messenger {
fn new() -> Self {
Self {
messages: RefCell::new(vec![]),
}
}
// &self 是不可变引用,但可以修改内部数据
fn send(&self, msg: &str) {
self.messages.borrow_mut().push(msg.to_string());
}
fn get_messages(&self) -> std::cell::Ref<Vec<String>> {
self.messages.borrow()
}
}
fn main() {
let messenger = Messenger::new();
messenger.send("你好");
messenger.send("世界");
println!("消息: {:?}", messenger.get_messages());
}
🔄 10.6 组合使用:常见模式
🌳 10.6.1 Rc<RefCell<T>>:共享可变数据
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
}
impl Node {
fn new(value: i32) -> Rc<Self> {
Rc::new(Node {
value,
children: RefCell::new(vec![]),
})
}
fn add_child(parent: &Rc<Self>, child: Rc<Self>) {
parent.children.borrow_mut().push(child);
}
}
fn main() {
let root = Node::new(1);
let child1 = Node::new(2);
let child2 = Node::new(3);
Node::add_child(&root, child1);
Node::add_child(&root, child2);
// 共享子节点
let shared = Node::new(4);
Node::add_child(&root.children.borrow()[0], Rc::clone(&shared));
Node::add_child(&root.children.borrow()[1], Rc::clone(&shared));
println!("根节点: {}", root.value);
for child in root.children.borrow().iter() {
println!(" 子节点: {}", child.value);
for grandchild in child.children.borrow().iter() {
println!(" 孙节点: {}", grandchild.value);
}
}
}
🧵 10.6.2 Arc<Mutex<T>>:线程安全的共享可变
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("结果: {}", *counter.lock().unwrap()); // 10
}
比喻时刻:
Mutex就像一个单人卫生间——同一时间只允许一个人进入(持有锁)。其他人必须等待当前的人离开(释放锁)后才能进入。
📚 10.7 其他智能指针
📝 10.7.1 Cow<T>:写时克隆
use std::borrow::Cow;
fn process(input: Cow<str>) -> Cow<str> {
if input.contains("old") {
// 需要修改时才分配新内存
let mut owned = input.into_owned();
owned = owned.replace("old", "new");
Cow::Owned(owned)
} else {
// 不修改时直接返回引用
input
}
}
fn main() {
let borrowed = Cow::Borrowed("hello old world");
let result = process(borrowed);
println!("{}", result); // hello new world
let no_change = Cow::Borrowed("hello world");
let result2 = process(no_change);
println!("{}", result2); // hello world(未分配)
}
🎯 10.7.2 Cell<T>:简单内部可变性
use std::cell::Cell;
fn main() {
let cell = Cell::new(5);
// Cell 适用于 Copy 类型,开销更小
cell.set(10);
println!("值: {}", cell.get());
// 原子替换
let old = cell.replace(20);
println!("旧值: {}, 新值: {}", old, cell.get());
}
📊 10.7.3 Weak<T>:避免循环引用
use std::cell::RefCell;
use std::rc::{Rc, Weak};
struct Node {
value: i32,
parent: RefCell<Weak<Node>>, // 弱引用,不增加引用计数
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let root = Rc::new(Node {
value: 1,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
let child = Rc::new(Node {
value: 2,
parent: RefCell::new(Rc::downgrade(&root)), // 弱引用父节点
children: RefCell::new(vec![]),
});
root.children.borrow_mut().push(Rc::clone(&child));
// 通过弱引用访问父节点
if let Some(parent) = child.parent.borrow().upgrade() {
println!("父节点值: {}", parent.value);
}
}
🧪 10.8 实战:构建带缓存的计算器
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
struct Calculator {
cache: RefCell<HashMap<String, f64>>,
operations: Vec<Box<dyn Fn(f64, f64) -> f64>>,
}
impl Calculator {
fn new() -> Self {
Self {
cache: RefCell::new(HashMap::new()),
operations: vec![
Box::new(|a, b| a + b),
Box::new(|a, b| a - b),
Box::new(|a, b| a * b),
Box::new(|a, b| if b != 0.0 { a / b } else { f64::NAN }),
],
}
}
fn compute(&self, a: f64, op_idx: usize, b: f64) -> f64 {
let key = format!("{}_{}_{}", a, op_idx, b);
// 检查缓存
if let Some(&result) = self.cache.borrow().get(&key) {
println!("从缓存获取: {} = {}", key, result);
return result;
}
// 计算
let result = if let Some(op) = self.operations.get(op_idx) {
op(a, b)
} else {
f64::NAN
};
// 存入缓存
self.cache.borrow_mut().insert(key.clone(), result);
println!("计算并缓存: {} = {}", key, result);
result
}
fn clear_cache(&self) {
self.cache.borrow_mut().clear();
println!("缓存已清空");
}
fn cache_size(&self) -> usize {
self.cache.borrow().len()
}
}
fn main() {
let calc = Rc::new(Calculator::new());
println!("=== 第一次计算 ===");
println!("2 + 3 = {}", calc.compute(2.0, 0, 3.0));
println!("5 - 1 = {}", calc.compute(5.0, 1, 1.0));
println!("\n=== 重复计算(命中缓存)===");
println!("2 + 3 = {}", calc.compute(2.0, 0, 3.0));
println!("\n=== 当前缓存大小: {} ===", calc.cache_size());
// 共享计算器
let calc2 = Rc::clone(&calc);
println!("\n=== 在另一个引用上计算 ===");
println!("4 * 5 = {}", calc2.compute(4.0, 2, 5.0));
println!("缓存大小: {}", calc.cache_size());
// 清空缓存
calc2.clear_cache();
println!("清空后缓存大小: {}", calc.cache_size());
}
📝 本章小结
本章我们学习了 Rust 的智能指针:
- <code>Box<T></code> 堆分配,用于递归类型和 Trait 对象
- <code>Rc<T></code> 引用计数共享所有权(单线程)
- <code>Arc<T></code> 原子引用计数共享所有权(多线程)
- <code>RefCell<T></code> 内部可变性,运行时借用检查
- <code>Mutex<T></code> 互斥锁,线程安全的内部可变性
- <code>Weak<T></code> 弱引用,避免循环引用
关键要点:
- 智能指针拥有数据,而不仅仅是借用
Dereftrait 让智能指针像引用一样使用Droptrait 自定义析构行为- 组合使用满足复杂场景需求
- 注意循环引用导致的内存泄漏
在下一章,我们将学习并发编程——Rust 的"无畏并发"哲学。
动手实验:
- 使用
Rc<RefCell<T>>实现一个双向链表节点结构。- 使用
Arc<Mutex<T>>实现一个多线程安全的计数器,让 5 个线程各增加 1000 次。- 解释为什么
RefCell的借用检查在运行时而非编译时,这有什么优缺点?