智能指针
Rust 中的指针是包含内存地址的变量,该地址指向另一块数据。最常见的指针是普通引用 &T,它只是借用数据,不持有所有权。智能指针在此基础上更进一步: 它是一个结构体,既保存了指向数据的地址,也拥有数据的所有权,并通过实现 Deref 和 Drop 两个 trait,使其能像普通引用一样使用,同时在离开作用域时自动执行清理逻辑。
你其实早已接触过智能指针——String 和 Vec<T> 本质上都是智能指针: 它们在堆上管理动态内存,持有该内存的所有权,并在离开作用域时自动释放。与之对应的 &str 和 &[T] 只是普通的胖指针,借用数据但不拥有所有权。
| 特性 | 普通引用 &T | 智能指针(Box、Rc 等) |
|---|---|---|
| 本质 | 内存地址 | 包含地址 + 额外元数据的结构体 |
| 所有权 | 借用,不持有数据 | 拥有(或共同拥有)数据 |
| 自动清理 | 不负责清理 | 离开作用域时自动释放内存 |
| 额外能力 | 无 | 引用计数、内部可变性等 |
智能指针区别于普通结构体的核心在于它实现了
Dereftrait(让其能像引用一样使用)和Droptrait(定义离开作用域时的清理逻辑)。
Deref Trait: 解引用运算符
实现 Deref trait 的类型可以重载解引用运算符 *,使其能像普通引用一样被解引用。这是智能指针的核心 trait 之一——没有 Deref,Box<T> 就只是一个普通结构体,*box_val 将无法编译。
实现 Deref
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> { MyBox(x) }
}
// 实现 Deref,使 MyBox<T> 能像 &T 一样使用
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0 // 返回内部值的引用(而非值本身,以免转移所有权)
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, *y); // *y 等价于 *(y.deref())
}当你写 *y 时,编译器实际上展开为 *(y.deref())——先调用 deref() 得到一个常规引用,再用 * 解引用。deref() 返回引用而非值,是为了避免将内部数据的所有权转移给调用者。
*不会无限递归替换: 从*y到*(y.deref())只发生一次。
Deref 隐式转换
对于函数和方法的传参,Rust 提供了隐式 Deref 转换: 当实参类型与参数签名不匹配,但实参类型实现了 Deref 时,编译器会自动沿着 Deref 链反复调用 deref() 直到类型匹配:
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
// 隐式转换链(编译期完成,零运行时开销):
// &MyBox<String> --[MyBox 的 Deref]--> &String
// &String --[String 的 Deref]--> &str ✅
}若没有隐式转换,同样的调用需要写成 hello(&(*m)[..]),可读性大幅下降。& 是触发隐式 Deref 转换的信号——只有引用类型的实参才会触发自动解引用。
引用归一化
&T 本身也实现了 Deref<Target=T>,因此 Rust 编译器会自动将多重引用 &&&&T 归一化为 &T:
struct Foo;
impl Foo {
fn method(&self) { println!("called"); }
}
let f = &&Foo;
f.method(); // ✅ &&Foo 自动归一化为 &Foo
(&&&&f).method(); // ✅ 同理,任意层级的引用都能自动解引用三种 Deref 转换规则
| 条件 | 转换 |
|---|---|
T: Deref<Target=U> | &T → &U |
T: DerefMut<Target=U> | &mut T → &mut U |
T: Deref<Target=U> | &mut T → &U |
第三条允许可变引用隐式转为不可变引用,但反向不行——如果不可变引用能转为可变,就会与其他已有的不可变引用发生冲突,破坏借用规则。
DerefMut 的用法与 Deref 类似,只是针对可变引用:
use std::ops::{Deref, DerefMut};
struct MyBox<T> { v: T }
// 实现 Deref 使 MyBox<T> 能像 &T 一样使用
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T { &self.v }
}
// 实现 DerefMut 使 MyBox<T> 能像 &mut T 一样使用
impl<T> DerefMut for MyBox<T> {
fn deref_mut(&mut self) -> &mut T { &mut self.v }
}
fn append(s: &mut String) {
s.push_str(", world");
}
fn main() {
let mut m = MyBox { v: String::from("hello") };
append(&mut m); // &mut MyBox<String> → &mut String(DerefMut 隐式转换)
println!("{}", m.v); // "hello, world"
}Drop Trait: 自动清理逻辑
Drop trait 类似于其他语言的析构函数,定义了值离开作用域时自动执行的清理代码。几乎所有涉及资源管理的类型(文件句柄、网络连接、堆内存等)都应实现 Drop:
struct CustomPointer {
data: String,
}
// 实现 Drop,在 CustomPointer 离开作用域时自动执行清理逻辑
impl Drop for CustomPointer {
fn drop(&mut self) {
println!("清理 CustomPointer,data = {}", self.data);
}
}
fn main() {
let c = CustomPointer { data: String::from("重要资源") };
let d = CustomPointer { data: String::from("另一个资源") };
println!("创建完毕");
}
// 输出:
// 创建完毕
// 清理 CustomPointer,data = 另一个资源 ← d(后创建,先释放)
// 清理 CustomPointer,data = 重要资源 ← c(先创建,后释放)Drop 的顺序
变量按照创建顺序的逆序(后进先出)释放;结构体内部字段按照定义顺序依次释放:
struct A;
struct B;
impl Drop for A { fn drop(&mut self) { println!("drop A"); } }
impl Drop for B { fn drop(&mut self) { println!("drop B"); } }
struct Pair { first: A, second: B }
impl Drop for Pair { fn drop(&mut self) { println!("drop Pair"); } }
fn main() {
let _p = Pair { first: A, second: B }; // 先创建
let _b = B; // 后创建
println!("运行中");
}
// 输出:
// 运行中
// drop B ← _b(后创建的变量,先释放)
// drop Pair ← _p 自身的 drop 实现
// drop A ← _p.first(按字段定义顺序)
// drop B ← _p.second即使 Pair 没有实现 Drop,Rust 也会自动为其字段依次调用 drop。
提前释放: std::mem::drop
禁止直接调用 .drop() 方法(编译器会阻止,因为这会导致二次释放(double free))。若需提前手动释放,使用标准库的 drop(value) 函数:
fn main() {
let lock = CustomPointer { data: String::from("锁资源") };
// lock.drop(); // ❌ 编译错误: explicit use of destructor method
drop(lock); // ✅ 转移所有权,函数结束时触发清理
println!("锁已提前释放,其他代码可以获得锁");
}std::mem::drop 的实现极其简单,仅靠所有权规则来触发 drop:
pub fn drop<T>(_x: T) {} // 拿走所有权,函数结束时 _x 被 dropCopy 与 Drop 互斥
一个类型不能同时实现 Copy 和 Drop。Copy 类型会被隐式复制,难以预测析构函数执行的时机,因此 Rust 明确禁止:
#[derive(Copy)]
struct Foo;
impl Drop for Foo { // ❌ 编译错误: the trait `Copy` may not be implemented for
fn drop(&mut self) { println!("dropping"); } // this type; the type has a destructor
}Box<T>: 将数据分配到堆上
Box<T> 是 Rust 中最简单的智能指针,它将值存储在堆上,在栈上保留一个指向堆数据的固定大小指针。除了堆分配本身的开销,Box<T> 没有任何额外运行时代价。
基本用法
Box::new(value)创建一个新的Box<T>智能指针,将value存储在堆上。
fn main() {
let b = Box::new(5); // 5 存储在堆上,b(指针)在栈上
println!("b = {}", b); // Box 实现了 Deref,可以直接打印
} // b 离开作用域,堆上的 5 被自动释放场景一: 递归类型
Rust 在编译时必须知道每个类型的精确大小。递归类型(在自身定义中包含同类型值)会导致大小无限递归,编译器无法确定:
enum List {
Cons(i32, List), // ❌ 错误: recursive type `List` has infinite size
Nil,
}Box<T> 是固定大小的指针(在 64 位系统上为 8 字节),将递归部分装箱即可打破这个无限递归:
enum List {
Cons(i32, Box<List>), // ✅ Box 是固定大小的指针
Nil,
}
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}场景二: 大数据零拷贝转移
在栈上转移所有权时,Rust 默认会深拷贝整个数据(从一个栈帧复制到另一个)。对于大型数据,这非常耗时。将数据放在堆上,转移所有权时只需复制栈上的指针(8 字节):
fn main() {
// 栈上数组: 转移所有权时发生深拷贝(整个数组被复制一份)
let arr1 = [0u32; 1000];
let arr2 = arr1;
println!("{}", arr1.len()); // ✅ arr1 仍可用(因为 [u32; N] 实现了 Copy)
// 堆上数据: 转移所有权时只复制栈上指针(8 字节),堆数据不动
let large = Box::new([0u32; 1000]);
let large2 = large; // 所有权转移,仅复制指针
// println!("{}", large.len()); // ❌ large 已失效
println!("{}", large2.len()); // ✅
}场景三: Trait 对象(类型擦除)
当你需要在集合中同时存储不同具体类型,但这些类型都实现了同一个 trait 时,Box<dyn Trait> 是最常用的解决方案。Vec 要求元素大小一致,不同具体类型大小不同;而 Box 本身大小固定(一个指针),完美解决这个问题:
trait Draw {
fn draw(&self);
}
struct Button { id: u32 }
impl Draw for Button {
fn draw(&self) { println!("绘制按钮 {}", self.id); }
}
struct Select { id: u32 }
impl Draw for Select {
fn draw(&self) { println!("绘制选择框 {}", self.id); }
}
fn main() {
// Button 和 Select 大小不同,但 Box 大小固定
let widgets: Vec<Box<dyn Draw>> = vec![
Box::new(Button { id: 1 }),
Box::new(Select { id: 2 }),
];
for w in &widgets {
w.draw();
}
}Box::leak: 创建 'static 引用
Box::leak 消耗掉 Box,使其指向的值不再受 Drop 管理,从而获得 'static 生命周期的引用。这在需要"运行时初始化、全局有效"的配置或单例场景中很有用:
fn get_config() -> &'static str {
let mut s = String::new();
s.push_str("database_url=localhost");
Box::leak(s.into_boxed_str()) // 返回 &'static str
}
fn main() {
let config = get_config();
println!("{}", config); // "database_url=localhost"
}
Box::leak并不是"内存泄漏"的不良实践,而是一种有意为之的设计,将运行期动态生成的值提升为全局有效。相比Rc/Arc,它的性能开销更低。
Rc<T>: 引用计数智能指针
Rust 的所有权规则要求一个值只能有一个所有者。但在图结构、多个组件共享配置等场景中,一个数据需要被多处同时持有。Rc<T>(Reference Counted)通过引用计数解决了这个问题: 每多一个所有者,计数加 1;计数降为 0 时,数据才被释放。
Rc<T>仅适用于单线程场景,因为引用计数操作没有原子性保证。多线程场景请使用Arc<T>(见无畏并发章节)。
基本用法
Rc::new(value)创建一个新的Rc<T>智能指针。Rc::clone(&rc)增加引用计数,而不是进行深拷贝。Rc::strong_count(&rc)获取当前的强引用计数。
use std::rc::Rc;
fn main() {
let a = Rc::new(String::from("共享数据"));
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 离开作用域,计数减 1
println!("引用计数 = {}", Rc::strong_count(&a)); // 2
} // b 和 a 离开作用域,计数归 0,内存释放Rc::clone(&a) 不进行深拷贝,只是增加引用计数并返回一个新的指向相同数据的指针,代价极低。使用 Rc::clone 而非 a.clone() 是一种惯例,明确提示读者"这里不做深拷贝"。
实际场景: 多所有者共享数据
use std::rc::Rc;
struct Owner { name: String }
struct Gadget { id: i32, owner: Rc<Owner> }
fn main() {
let owner = Rc::new(Owner { name: "Gadget Man".to_string() });
let g1 = Gadget { id: 1, owner: Rc::clone(&owner) };
let g2 = Gadget { id: 2, owner: Rc::clone(&owner) };
drop(owner); // 释放原始变量(引用计数从 3 降到 2),数据仍然有效
println!("Gadget {} owned by {}", g1.id, g1.owner.name); // 仍可访问
println!("Gadget {} owned by {}", g2.id, g2.owner.name);
} // g1、g2 离开作用域,计数归 0,Owner 才被释放只读限制
Rc<T> 只提供对数据的不可变引用(&T)。这符合 Rust 的借用规则: 多个所有者同时存在,意味着有多个引用,必须都是不可变的。若需要修改,需配合 RefCell<T> 使用(见下节)。
RefCell<T> 与内部可变性
内部可变性(Interior Mutability)允许在持有不可变引用的情况下修改数据。RefCell<T> 实现了这一模式,它将借用规则的检查从编译期推迟到运行时: 若在运行时违反借用规则(如同时持有可变和不可变引用),程序会 panic。
Box<T> | RefCell<T> | |
|---|---|---|
| 借用检查时机 | 编译期 | 运行时 |
| 违规后果 | 编译错误 | 运行时 panic |
| 可变性 | 遵循普通借用规则 | 即使外部不可变也能获得可变借用 |
基本用法
RefCell::new(value)创建一个新的RefCell<T>智能指针。borrow()获取不可变借用,返回Ref<T>。borrow_mut()获取可变借用,返回RefMut<T>。
经典场景: Mock 测试
外部库定义了一个 Messenger trait,其 send 方法签名是 &self(不可变)。你需要在自己的 MockMessenger 实现中记录发送的消息,但 &self 不允许修改字段:
struct MockMessenger { sent: Vec<String> }
pub trait Messenger {
fn send(&self, msg: &str); // 签名固定,无法修改
}
// ❌ 无法在 &self 方法中调用 push
struct BadMock { sent: Vec<String> }
impl Messenger for BadMock {
fn send(&self, msg: &str) {
self.sent.push(msg.to_string()); // 错误: self 是不可变引用
}
}将字段包裹在 RefCell<T> 中,通过 borrow_mut() 在不可变 self 内部获得可变访问:
use std::cell::RefCell;
struct MockMessenger {
sent: RefCell<Vec<String>>, // ✅ 使用 RefCell 包裹 Vec<String>,允许内部可变
}
pub trait Messenger {
fn send(&self, msg: &str); // 签名固定,无法修改
}
impl Messenger for MockMessenger {
fn send(&self, msg: &str) {
self.sent.borrow_mut().push(msg.to_string()); // ✅ 内部可变性
}
}
fn main() {
let m = MockMessenger { sent: RefCell::new(Vec::new()) };
m.send("hello");
m.send("world");
println!("发送了 {} 条消息", m.sent.borrow().len()); // 2
}RefCell<T> 的两个核心方法:
borrow()→ 返回Ref<T>(不可变借用,可同时存在多个)borrow_mut()→ 返回RefMut<T>(可变借用,同一时刻只能有一个,且与不可变借用互斥)
运行时 panic
use std::cell::RefCell;
fn main() {
let data = RefCell::new(5);
let _a = data.borrow(); // 不可变借用
let _b = data.borrow_mut(); // ❌ 可编译,但运行时 panic!违反借用规则
// thread 'main' (12) panicked at src/main.rs:6:19: RefCell already borrowed
}
RefCell<T>并不绕过借用规则,只是将检查推迟到运行时。当你确信逻辑上不会发生冲突,但编译器无法静态推断时,才使用它。
Cell<T>: Copy 类型的轻量内部可变
Cell<T> 是比 RefCell<T> 更轻量的内部可变性工具,仅适用于实现了 Copy 的类型(如 i32、bool、&str)。
通过 .get() 和 .set() 直接操作值,不涉及引用,也没有借用状态检查,零运行时开销:
use std::cell::Cell;
fn main() {
let x = Cell::new(5);
let a = x.get(); // 取值(Copy 语义)
x.set(10); // 修改值,即使 x 是不可变绑定
let b = x.get();
println!("{a}, {b}"); // 5, 10
}Cell<T> | RefCell<T> | |
|---|---|---|
| 适用类型 | 仅 Copy 类型 | 任意类型 |
| 访问方式 | .get() / .set()(值操作) | .borrow() / .borrow_mut()(引用操作) |
| 运行时开销 | 无 | 有(借用计数检查) |
| 违规行为 | 不会 panic(无引用概念) | 运行时 panic |
需要内部可变性时,优先选
Cell<T>;只有类型没有实现Copy时,才选RefCell<T>。
Rc<T> + RefCell<T> 组合
这是最常见的智能指针组合: Rc<T> 提供多所有权,RefCell<T> 提供内部可变性,合起来实现多个所有者共享同一块可变数据:
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
let shared = Rc::new(RefCell::new("初始值".to_string()));
let a = Rc::clone(&shared);
let b = Rc::clone(&shared);
a.borrow_mut().push_str(",A 修改了");
b.borrow_mut().push_str(",B 也修改了");
// 所有所有者看到的是同一份数据
println!("{}", shared.borrow());
// "初始值,A 修改了,B 也修改了"
}这种组合的性能开销很低: 内存上仅多分配 3 个 usize/isize(Rc 的强/弱计数 + RefCell 的借用状态),CPU 损耗也非常小。
引用循环与内存泄漏
同时使用 Rc<T> 和 RefCell<T> 时存在一个陷阱: 引用循环。当两个 Rc 互相持有对方时,引用计数永远不会降为 0,造成内存泄漏:
use std::rc::Rc;
use std::cell::RefCell;
struct Node {
next: RefCell<Option<Rc<Node>>>,
}
fn main() {
// 1. 创建节点 a
let a: Rc<Node> = Rc::new(Node { next: RefCell::new(None) });
// 2. 创建节点 b,并让 b 指向 a
let b: Rc<Node> = Rc::new(Node { next: RefCell::new(Some(Rc::clone(&a))) });
// 3. 让 a 指向 b —— 此时形成了闭环:a -> b -> a
*a.next.borrow_mut() = Some(Rc::clone(&b));
// 此时 a 和 b 的强引用计数都是 2
// 当 main 函数结束时,a 和 b 的变量被销毁,计数减 1,但仍各剩 1
// 内存永远不会被回收,发生了内存泄漏!
}使用 Weak<T> 打破循环
Weak<T> 是弱引用: 它不增加强引用计数(strong_count),只增加弱引用计数(weak_count),不持有所有权,也不阻止数据被释放。访问数据时必须先调用 upgrade() 方法,返回 Option<Rc<T>>——数据存在则为 Some,已释放则为 None。
Rc<T> | Weak<T> | |
|---|---|---|
| 增加计数类型 | strong_count | weak_count |
| 持有所有权 | 是 | 否 |
| 阻止数据释放 | 是 | 否 |
| 访问方式 | 直接解引用 | 必须先 .upgrade() 获取 Option<Rc<T>> |
基本用法
Weak::new()创建一个空的Weak<T>指针。Rc::downgrade(&rc)创建一个Weak<T>指向Rc<T>,但不增加强引用计数。weak.upgrade()尝试获取一个Rc<T>,如果原数据仍然存在(强引用计数 > 0),返回Some(Rc<T>);如果数据已被释放(强引用计数 = 0),返回None。
use std::rc::{Rc, Weak};
use std::cell::RefCell;
struct Node {
// 将此处改为 Weak 以打破循环
next: RefCell<Option<Weak<Node>>>,
}
fn main() {
// 1. 创建节点 a
let a = Rc::new(Node { next: RefCell::new(None) });
// 2. 创建节点 b,并让 b 指向 a
// 注意: 这里 b -> a 使用了 downgrade 将 Rc 转为 Weak
let b = Rc::new(Node {next: RefCell::new(Some(Rc::downgrade(&a)))});
// 3. 让 a 指向 b
// a -> b 保持强引用,b -> a 是弱引用,不再形成闭环
*a.next.borrow_mut() = Some(Rc::downgrade(&b));
// 验证引用计数:
// a 的强引用计数为 1(变量 a),弱引用计数为 1(b.next)
// b 的强引用计数为 1(变量 b),弱引用计数为 1(a.next)
println!("a strong count: {}", Rc::strong_count(&a)); // 1
// 当 main 结束,a 和 b 的强引用计数归零,内存将被正常回收.
}实例: 树形结构中的父子引用
典型用法是父节点通过 Rc 持有子节点(强引用),子节点通过 Weak 反向引用父节点(弱引用),从而避免循环:
use std::rc::{Rc, Weak};
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>, // 弱引用: 指向父节点,不持有所有权
children: RefCell<Vec<Rc<Node>>>, // 强引用: 持有子节点
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
{
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
// 子节点通过 Weak 反向引用父节点
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!("branch: strong={}, weak={}", Rc::strong_count(&branch), Rc::weak_count(&branch));
// strong=1(只有变量 branch),weak=1(leaf.parent 持有)
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
// Some(Node { value: 5, ... })
}
// branch 离开作用域,strong_count 归 0,branch 被释放
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
// None(branch 已被释放,Weak 自动失效)
}对于树/图等层级结构中的反向引用(子 → 父),始终使用
Weak;正向引用(父 → 子)使用Rc。这是避免循环引用的通用模式。