Rust从入门到精通之进阶篇:16.智能指针
智能指针
智能指针是一种数据结构,它们的行为类似于指针,但具有额外的元数据和功能。在 Rust 中,智能指针通常实现了 Deref
和 Drop
特质,允许它们像引用一样工作并在离开作用域时自动清理资源。在本章中,我们将探索 Rust 中的各种智能指针类型及其用途。
引用回顾
在深入智能指针之前,让我们先回顾一下 Rust 中的普通引用:
fn main() {
let x = 5;
let y = &x; // y 是 x 的引用
assert_eq!(5, x);
assert_eq!(5, *y); // 使用解引用运算符 * 访问引用的值
}
引用是 Rust 中最简单的指针类型,它们没有任何特殊功能,只是借用值而不获取所有权。
Box
Box<T>
是 Rust 中最简单的智能指针类型,它允许你将值存储在堆上而不是栈上:
fn main() {
let b = Box::new(5); // 在堆上分配值 5
println!("b = {}", b);
// 可以像使用引用一样使用 Box
assert_eq!(5, *b);
} // b 离开作用域时,它指向的堆内存会被自动释放
Box 的主要用途
1. 存储已知大小但较大的数据
当你有一个较大的数据结构,但不想在栈上分配内存时,可以使用 Box
:
struct LargeStruct {
data: [u8; 1000000], // 1MB 的数据
}
fn main() {
// 在栈上分配可能导致栈溢出
// let large_struct = LargeStruct { data: [0; 1000000] };
// 在堆上分配更安全
let large_struct = Box::new(LargeStruct { data: [0; 1000000] });
println!("结构体已创建");
}
2. 创建递归类型
递归类型的大小在编译时无法确定,因此需要使用 Box
来创建:
enum List {
Cons(i32, Box<List>),
Nil,
}
fn main() {
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Cons(3, Box::new(List::Nil))))));
// 使用模式匹配访问列表元素
let mut current = &list;
while let List::Cons(value, next) = current {
println!("值: {}", value);
current = next;
}
}
3. 特质对象
Box
可以用来创建特质对象,允许在运行时使用动态分发:
trait Draw {
fn draw(&self);
}
struct Circle {
radius: f64,
}
impl Draw for Circle {
fn draw(&self) {
println!("画一个半径为 {} 的圆", self.radius);
}
}
struct Square {
side: f64,
}
impl Draw for Square {
fn draw(&self) {
println!("画一个边长为 {} 的正方形", self.side);
}
}
fn main() {
let shapes: Vec<Box<dyn Draw>> = vec![
Box::new(Circle { radius: 1.0 }),
Box::new(Square { side: 2.0 }),
];
for shape in shapes {
shape.draw();
}
}
Deref 特质
Deref
特质允许自定义解引用运算符 *
的行为。智能指针通过实现 Deref
特质,使它们的行为类似于引用:
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) -> &Self::Target {
&self.0
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y); // 相当于 *(y.deref())
}
解引用强制转换
Rust 提供了解引用强制转换(deref coercion)功能,当将一个实现了 Deref
特质的类型的值作为参数传递给函数或方法时,如果参数类型不匹配,Rust 会自动应用 deref
方法:
fn hello(name: &str) {
println!("你好,{}!", name);
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m); // 解引用强制转换:&MyBox<String> -> &String -> &str
}
Drop 特质
Drop
特质允许你自定义当值离开作用域时发生的行为。这对于释放资源(如文件句柄或网络连接)非常有用:
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("释放 CustomSmartPointer,数据: `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer { data: String::from("我的数据") };
let d = CustomSmartPointer { data: String::from("其他数据") };
println!("创建了智能指针");
// c 和 d 在这里离开作用域,Rust 会自动调用它们的 drop 方法
}
提前丢弃值
有时你可能需要提前丢弃一个值。Rust 提供了 std::mem::drop
函数来实现这一点:
fn main() {
let c = CustomSmartPointer { data: String::from("提前丢弃") };
println!("创建了智能指针");
drop(c); // 手动调用 drop 函数
println!("在 main 函数结束前丢弃了智能指针");
}
Rc
Rc<T>
(引用计数,Reference Counting)允许多个所有者共享同一数据的所有权。当最后一个所有者离开作用域时,数据才会被清理:
use std::rc::Rc;
fn main() {
let a = Rc::new(5); // 创建一个引用计数的值
println!("创建 a,引用计数 = {}", Rc::strong_count(&a)); // 1
let b = Rc::clone(&a); // 增加引用计数,而不是复制数据
println!("创建 b,引用计数 = {}", Rc::strong_count(&a)); // 2
{
let c = Rc::clone(&a); // 再次增加引用计数
println!("创建 c,引用计数 = {}", Rc::strong_count(&a)); // 3
} // c 离开作用域,引用计数减少
println!("c 离开作用域后,引用计数 = {}", Rc::strong_count(&a)); // 2
// 可以通过任何一个引用访问数据
println!("a = {}, b = {}", a, b);
} // a 和 b 离开作用域,引用计数变为 0,数据被清理
Rc 的限制
Rc<T>
只能用于单线程场景,并且只提供不可变访问:
use std::rc::Rc;
fn main() {
let a = Rc::new(vec![1, 2, 3]);
let b = Rc::clone(&a);
// 错误:不能获取可变引用
// a.push(4);
println!("a = {:?}, b = {:?}", a, b);
}
RefCell 和内部可变性
内部可变性是 Rust 的一种设计模式,它允许你在拥有不可变引用的情况下修改数据。RefCell<T>
提供了内部可变性:
use std::cell::RefCell;
fn main() {
let data = RefCell::new(5);
// 获取不可变引用
let a = data.borrow();
println!("a = {}", a);
// 必须先释放不可变引用,才能获取可变引用
drop(a);
// 获取可变引用并修改值
let mut b = data.borrow_mut();
*b += 1;
println!("b = {}", b);
}
借用规则在运行时检查
与普通引用不同,RefCell<T>
在运行时而不是编译时检查借用规则:
use std::cell::RefCell;
fn main() {
let data = RefCell::new(5);
let a = data.borrow();
let b = data.borrow(); // 可以同时有多个不可变借用
println!("a = {}, b = {}", a, b);
// 下面的代码会在运行时 panic,因为已经有不可变借用
// let mut c = data.borrow_mut();
}
结合 Rc 和 RefCell
Rc<T>
和 RefCell<T>
经常一起使用,以提供多所有者和内部可变性:
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
let data = Rc::new(RefCell::new(vec![1, 2, 3]));
let a = Rc::clone(&data);
let b = Rc::clone(&data);
// 通过 a 修改数据
a.borrow_mut().push(4);
// 通过 b 也能看到修改后的数据
println!("b = {:?}", b.borrow());
}
Weak
Weak<T>
提供了对 Rc<T>
数据的非所有权引用,不会增加强引用计数,因此不会阻止数据的清理:
use std::rc::{Rc, Weak};
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
parent: Option<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: None,
children: RefCell::new(vec![]),
});
println!("leaf 强引用计数 = {}", Rc::strong_count(&leaf)); // 1
println!("leaf 弱引用计数 = {}", Rc::weak_count(&leaf)); // 0
{
let branch = Rc::new(Node {
value: 5,
parent: None,
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
// 设置 leaf 的父节点为 branch,使用 Weak 引用避免循环引用
leaf.parent = Some(Rc::downgrade(&branch));
println!("branch 强引用计数 = {}", Rc::strong_count(&branch)); // 1
println!("branch 弱引用计数 = {}", Rc::weak_count(&branch)); // 1
println!("leaf 强引用计数 = {}", Rc::strong_count(&leaf)); // 2
println!("leaf 弱引用计数 = {}", Rc::weak_count(&leaf)); // 0
// 访问 leaf 的父节点
if let Some(parent) = &leaf.parent {
// 尝试将 Weak 引用升级为 Rc
if let Some(parent) = parent.upgrade() {
println!("leaf 的父节点是 {}", parent.value);
}
}
} // branch 离开作用域,强引用计数变为 0,数据被清理
// branch 已被清理,所以 leaf.parent 现在是悬空的 Weak 引用
println!("leaf 强引用计数 = {}", Rc::strong_count(&leaf)); // 1
// 尝试访问已清理的父节点
if let Some(parent) = &leaf.parent {
if let Some(_) = parent.upgrade() {
println!("leaf 的父节点仍然存在");
} else {
println!("leaf 的父节点已被清理");
}
}
}
Arc
Arc<T>
(原子引用计数,Atomic Reference Counting)是 Rc<T>
的线程安全版本,可以在多线程环境中安全地共享数据:
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec![1, 2, 3]);
let mut handles = vec![];
for i in 0..3 {
let data_clone = Arc::clone(&data);
let handle = thread::spawn(move || {
println!("线程 {}: 数据 = {:?}", i, data_clone);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
}
Mutex
Mutex<T>
(互斥锁)提供了线程安全的内部可变性,确保在任何时刻只有一个线程可以访问数据:
use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Mutex::new(0);
let mut handles = vec![];
for _ in 0..10 {
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("结果: {}", *counter.lock().unwrap());
}
上面的代码会编译失败,因为 counter
的所有权在第一个线程中被移动。要解决这个问题,我们需要结合 Arc
和 Mutex
:
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_clone = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter_clone.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("结果: {}", *counter.lock().unwrap());
}
RwLock
RwLock<T>
(读写锁)允许多个读取器或一个写入器访问数据:
use std::sync::{Arc, RwLock};
use std::thread;
fn main() {
let data = Arc::new(RwLock::new(vec![1, 2, 3]));
let mut handles = vec![];
// 创建多个读取线程
for i in 0..3 {
let data_clone = Arc::clone(&data);
let handle = thread::spawn(move || {
let data = data_clone.read().unwrap();
println!("读取线程 {}: 数据 = {:?}", i, *data);
});
handles.push(handle);
}
// 创建一个写入线程
let data_clone = Arc::clone(&data);
let handle = thread::spawn(move || {
let mut data = data_clone.write().unwrap();
data.push(4);
println!("写入线程: 数据 = {:?}", *data);
});
handles.push(handle);
for handle in handles {
handle.join().unwrap();
}
println!("最终数据: {:?}", *data.read().unwrap());
}
智能指针的选择
以下是选择合适智能指针的指南:
智能指针 | 所有权 | 可变性 | 线程安全 | 主要用途 |
---|---|---|---|---|
Box<T> | 单一所有者 | 可变或不可变 | 取决于 T | 堆分配、递归类型、特质对象 |
Rc<T> | 多所有者 | 不可变 | 否 | 单线程共享所有权 |
RefCell<T> | 单一所有者 | 内部可变性 | 否 | 单线程内部可变性 |
Arc<T> | 多所有者 | 不可变 | 是 | 多线程共享所有权 |
Mutex<T> | 单一所有者 | 内部可变性 | 是 | 多线程互斥访问 |
RwLock<T> | 单一所有者 | 内部可变性 | 是 | 多线程读写访问 |
自定义智能指针
你可以通过实现 Deref
和 Drop
特质来创建自己的智能指针:
use std::ops::{Deref, DerefMut};
use std::fmt;
struct SmartPointer<T> {
value: T,
name: String,
}
impl<T> SmartPointer<T> {
fn new(value: T, name: &str) -> SmartPointer<T> {
println!("创建智能指针 {}", name);
SmartPointer {
value,
name: name.to_string(),
}
}
}
impl<T> Deref for SmartPointer<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.value
}
}
impl<T> DerefMut for SmartPointer<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.value
}
}
impl<T> Drop for SmartPointer<T> {
fn drop(&mut self) {
println!("释放智能指针 {}", self.name);
}
}
impl<T: fmt::Display> fmt::Display for SmartPointer<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "SmartPointer({}: {})", self.name, self.value)
}
}
fn main() {
let mut a = SmartPointer::new(5, "a");
let b = SmartPointer::new(10, "b");
println!("a = {}, b = {}", a, b);
println!("*a = {}, *b = {}", *a, *b);
*a += 1; // 使用 DerefMut
println!("修改后: a = {}", a);
}
最佳实践
1. 选择合适的智能指针
根据你的需求选择合适的智能指针:
- 需要在堆上分配数据?使用
Box<T>
- 需要共享所有权?使用
Rc<T>
或Arc<T>
- 需要内部可变性?使用
RefCell<T>
或Mutex<T>
/RwLock<T>
2. 避免循环引用
使用 Rc<T>
或 Arc<T>
时,要小心避免循环引用,这可能导致内存泄漏:
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
// 使用 Weak<T> 而不是 Rc<T> 避免循环引用
// children: RefCell<Vec<Rc<Node>>>,
// parent: Option<Rc<Node>>,
}
fn main() {
// 这会导致循环引用和内存泄漏
let a = Rc::new(RefCell::new(Node { value: 5 }));
let b = Rc::new(RefCell::new(Node { value: 10 }));
// a 引用 b
a.borrow_mut().children.push(Rc::clone(&b));
// b 引用 a
b.borrow_mut().parent = Some(Rc::clone(&a));
}
3. 使用 clone
而不是引用
使用 Rc::clone
或 Arc::clone
而不是引用,以明确表示你的意图:
use std::rc::Rc;
fn process(data: Rc<Vec<i32>>) {
println!("处理数据: {:?}", *data);
}
fn main() {
let data = Rc::new(vec![1, 2, 3]);
// 好的做法:使用 clone 明确表示共享所有权
process(Rc::clone(&data));
// 仍然可以使用原始数据
println!("原始数据: {:?}", *data);
}
4. 尽量减少锁的作用域
使用 Mutex
或 RwLock
时,尽量减少锁的作用域,以避免阻塞其他线程:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let data = Arc::new(Mutex::new(vec![1, 2, 3]));
let data_clone = Arc::clone(&data);
let handle = thread::spawn(move || {
// 不好的做法:锁的作用域太大
let mut data = data_clone.lock().unwrap();
// 执行耗时操作...
thread::sleep(std::time::Duration::from_secs(1));
data.push(4);
});
// 好的做法:减小锁的作用域
{
let mut data = data.lock().unwrap();
data.push(5);
} // 锁在这里被释放
// 执行其他操作...
handle.join().unwrap();
println!("最终数据: {:?}", *data.lock().unwrap());
}
5. 使用 parking_lot
库
考虑使用 parking_lot
库,它提供了更高性能的互斥原语:
// Cargo.toml
// [dependencies]
// parking_lot = "0.12.0"
use parking_lot::{Mutex, RwLock};
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let data_clone = Arc::clone(&data);
let handle = thread::spawn(move || {
// 不需要 unwrap,锁定失败会 panic
let mut num = data_clone.lock();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("结果: {}", *data.lock());
}
练习题
-
实现一个简单的二叉树数据结构,使用
Box<T>
存储节点。实现插入、搜索和遍历操作。 -
创建一个简单的对象池,使用
Rc<T>
和RefCell<T>
管理可重用对象。实现获取和释放对象的方法。 -
实现一个线程安全的计数器,使用
Arc<T>
和Mutex<T>
。创建多个线程增加计数器的值,并验证最终结果。 -
创建一个简单的观察者模式实现,其中主题使用
Weak<T>
引用存储观察者,以避免循环引用。 -
实现一个自定义智能指针,它可以记录解引用操作的次数。实现
Deref
、DerefMut
和Drop
特质,并在程序结束时打印统计信息。
总结
在本章中,我们探讨了 Rust 中的智能指针:
Box<T>
用于在堆上分配数据Rc<T>
用于单线程环境中的共享所有权RefCell<T>
用于单线程环境中的内部可变性Arc<T>
用于多线程环境中的共享所有权Mutex<T>
和RwLock<T>
用于多线程环境中的内部可变性Weak<T>
用于避免循环引用
我们还学