第4章 - 智能指针
嗨,朋友!我是长安。
这一章我们要学习 Rust 中一些非常有用的特殊类型——智能指针(Smart Pointers)。
智能指针和普通的引用不同,它们不仅仅是指向数据的指针,还拥有一些额外的元数据和能力。
🤔 什么是智能指针?
普通的引用只是“借用”数据,不拥有所有权。而智能指针通常:
- 拥有它指向的数据
- 离开作用域时会自动清理数据
- 有特殊的能力(比如引用计数、内部可变性等)
长安说
普通引用就像"借书卡",而智能指针像"书的所有权证书"。拥有智能指针,就拥有了数据。
最常用的智能指针有三个:
Box<T>- 堆上分配Rc<T>- 引用计数RefCell<T>- 内部可变性
让我们一个一个来看。
📦 Box<T> - 堆上分配
为什么需要 Box?
默认情况下,Rust 把数据存储在栈上。但有时候你需要把数据放在堆上:
- 数据太大,不想复制
- 编译时不知道数据的大小
- 想要转移所有权但不在乎具体类型
基本用法
fn main() {
let b = Box::new(5); // 在堆上分配一个 i32
println!("b = {}", b);
} // b 离开作用域,堆上的数据被清理
这看起来和普通变量没什么区别,但 Box 的真正威力在于处理递归类型。
递归类型示例:链表
不使用 Box 会报错:
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))))));
}
长安说
Box 的大小是固定的(就是一个指针的大小),所以编译器可以计算出结构体的大小。
实际应用:存储大对象
struct LargeStruct {
data: [u8; 10000], // 10KB 的数据
}
fn main() {
// 避免栈上分配大对象
let large = Box::new(LargeStruct {
data: [0; 10000],
});
println!("Large object allocated on heap!");
}
🔗 Rc<T> - 引用计数
为什么需要 Rc?
有时候一个值需要有多个所有者。比如图结构中,多个边可能指向同一个节点。
Rc<T> 就是 Reference Counted(引用计数)的缩写,它会记录有多少个引用指向这个值,只有当引用计数为 0 时才会清理数据。
注意
Rc<T> 只能用于单线程场景!多线程要用 Arc<T>(下一章会讲)。
基本用法
use std::rc::Rc;
fn main() {
let a = Rc::new(5);
println!("引用计数: {}", Rc::strong_count(&a)); // 1
let b = Rc::clone(&a); // 增加引用计数
println!("引用计数: {}", Rc::strong_count(&a)); // 2
{
let c = Rc::clone(&a);
println!("引用计数: {}", Rc::strong_count(&a)); // 3
} // c 离开作用域
println!("引用计数: {}", Rc::strong_count(&a)); // 2
} // a 和 b 离开作用域,计数为 0,数据被清理
长安说
Rc::clone(&a) 不会深拷贝数据,只是增加引用计数,非常快!
实际应用:共享数据
use std::rc::Rc;
enum List {
Cons(i32, Rc<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
// b 和 c 共享 a
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
println!("a 的引用计数: {}", Rc::strong_count(&a)); // 3
}
🔒 RefCell<T> - 内部可变性
为什么需要 RefCell?
记得借用规则吗?
- 同一时间,只能有一个可变引用,或多个不可变引用
但有时候你需要在拥有不可变引用时修改数据,RefCell<T> 就是为此设计的。
RefCell<T> 使用内部可变性模式:在运行时检查借用规则,而不是编译时。
注意
如果运行时违反借用规则,程序会 panic!
基本用法
use std::cell::RefCell;
fn main() {
let data = RefCell::new(5);
// 借用不可变引用
let value = data.borrow();
println!("value: {}", *value);
drop(value); // 释放借用
// 借用可变引用
let mut value_mut = data.borrow_mut();
*value_mut += 10;
drop(value_mut);
println!("modified: {}", *data.borrow());
}
实际应用:Mock 对象
use std::cell::RefCell;
trait Messenger {
fn send(&self, msg: &str);
}
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
// 虽然 self 是不可变的,但可以修改 RefCell 内部的数据
self.sent_messages.borrow_mut().push(String::from(message));
}
}
fn main() {
let messenger = MockMessenger::new();
messenger.send("消恗1");
messenger.send("消恗2");
assert_eq!(messenger.sent_messages.borrow().len(), 2);
}
🔥 组合使用:Rc<RefCell<T>>
最强大的组合:多个所有者 + 内部可变性!
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
children: RefCell::new(vec![]),
});
let branch = Rc::new(Node {
value: 5,
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
// leaf 有两个所有者:leaf 和 branch
println!("leaf 引用计数: {}", Rc::strong_count(&leaf)); // 2
// 修改 branch 的 children
branch.children.borrow_mut().push(Rc::clone(&leaf));
println!("leaf 引用计数: {}", Rc::strong_count(&leaf)); // 3
}
📊 智能指针对比
| 类型 | 所有权 | 可变性 | 检查时机 | 用途 |
|---|---|---|---|---|
Box<T> | 单一 | 编译时 | 编译时 | 堆上分配 |
Rc<T> | 多个 | 不可变 | 编译时 | 共享数据 |
RefCell<T> | 单一 | 内部可变 | 运行时 | 内部可变性 |
Arc<T> | 多个 | 不可变 | 编译时 | 线程安全共享 |
Mutex<T> | 多个 | 可变 | 运行时 | 线程安全可变 |
长安的选择指南
- 需要在堆上分配?→
Box<T> - 需要多个所有者?→
Rc<T>(单线程)或Arc<T>(多线程) - 需要内部可变?→
RefCell<T>(单线程)或Mutex<T>(多线程) - 需要多个所有者 + 可变?→
Rc<RefCell<T>>(单线程)或Arc<Mutex<T>>(多线程)
💡 小结
在这一章,我们学会了:
- 智能指针拥有数据并有额外能力
Box<T>:在堆上分配数据Rc<T>:允许多个所有者(引用计数)RefCell<T>:内部可变性,运行时检查借用规则Rc<RefCell<T>>:组合使用,多个所有者 + 可变性
核心要点:
Box<T>解决递归类型和大对象问题Rc<T>允许数据有多个所有者RefCell<T>在不可变引用下修改数据- 根据需求选择合适的智能指针
长安的建议
智能指针不难,关键是理解它们解决什么问题:
Box:最简单,就是把数据放堆上Rc:共享所有权,多个变量指向同一个数据RefCell:绕过编译时借用检查,在运行时检查
大多数情况下,Box 就够用了。只有在需要共享所有权或内部可变性时,才用 Rc 或 RefCell。
🚀 下一步
恭喜你掌握了智能指针!现在你已经掌握了 Rust 的大部分高级特性。
最后一章,我们要学习 Rust 最强大的特性之一——并发编程!
💪 练习题
- 使用
Box创建一个递归的二叉树结构 - 使用
Rc创建一个共享的配置对象,多个组件都能访问 - 使用
RefCell实现一个计数器,虽然对象是不可变的,但能增加计数 - 结合
Rc<RefCell<T>>创建一个可以共享且可修改的数据结构
答案示例
use std::cell::RefCell;
use std::rc::Rc;
// 练习1
enum BinaryTree {
Node(i32, Box<BinaryTree>, Box<BinaryTree>),
Leaf,
}
use BinaryTree::{Node, Leaf};
fn create_tree() -> BinaryTree {
Node(
1,
Box::new(Node(2, Box::new(Leaf), Box::new(Leaf))),
Box::new(Node(3, Box::new(Leaf), Box::new(Leaf))),
)
}
// 练习2
struct Config {
max_connections: u32,
timeout: u64,
}
fn main() {
let config = Rc::new(Config {
max_connections: 100,
timeout: 30,
});
let component1 = Rc::clone(&config);
let component2 = Rc::clone(&config);
println!("Component1 max: {}", component1.max_connections);
println!("Component2 timeout: {}", component2.timeout);
}
// 练习3
struct Counter {
count: RefCell<i32>,
}
impl Counter {
fn new() -> Self {
Counter {
count: RefCell::new(0),
}
}
fn increment(&self) {
*self.count.borrow_mut() += 1;
}
fn get(&self) -> i32 {
*self.count.borrow()
}
}
fn test_counter() {
let counter = Counter::new();
counter.increment();
counter.increment();
assert_eq!(counter.get(), 2);
}
// 练习4
#[derive(Debug)]
struct SharedData {
value: RefCell<i32>,
}
fn test_shared() {
let data = Rc::new(SharedData {
value: RefCell::new(0),
});
let data1 = Rc::clone(&data);
let data2 = Rc::clone(&data);
*data1.value.borrow_mut() += 10;
*data2.value.borrow_mut() += 5;
println!("Final value: {}", *data.value.borrow()); // 15
}
