Deref 与 Drop trait
本教程共 78 篇 · 第 61 篇 · 更新于 2026-08-08 · 约 10 分钟阅读
本节目标:理解 Deref 如何让智能指针像普通引用一样用
*解引用、并在函数传参时发生隐式转换;理解 Drop 如何定义值离开作用域时的自动清理,以及为什么不能手动调用 drop 方法而要用std::mem::drop函数。
上章说”智能指针”之所以智能,关键在它实现了两个 trait:Deref 和 Drop。有了它们,智能指针用起来和引用几乎没区别,还能在离开作用域时自动收尾。这章把这两个 trait 拆开讲透。
1-1 普通引用的解引用
先回顾最朴素的解引用。常规引用 & 保存的是目标值的内存地址,用 * 取出地址背后的数据:
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y); // *y 解引用拿到 5
}
若写 assert_eq!(5, y); 会报错——你没法拿一个引用和整数直接比,必须先 *y 解出来。
1-2 为智能指针实现 Deref
智能指针是个结构体,编译器不知道怎么对它 *。要像引用那样解引用,得自己实现 Deref。仿照 Box 做一个 MyBox:
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 y = MyBox::new(5);
assert_eq!(5, *y); // 现在能解引用了
}
deref 方法返回的是常规引用 &T。type Target = T 是关联类型,表示”解引用后得到的类型”。没实现 Deref 前,*y 会报 cannot be dereferenced。
Note
deref返回引用而不是值,是有深意的:如果它直接把值的所有权交出去,你只是想*T看一眼,里面的数据就被搬走了,这显然不对。返回引用,解引用的代价只是多一次指针跳转。
* 背后的真实操作是 *(y.deref()):先调 deref 拿到引用,再对引用解引用得到值。注意这只替换一次,不会无限递归成 *((y.deref()).deref())。
1-3 函数传参时的隐式 Deref 转换
Deref 最实用的地方是隐式转换。如果类型 T 实现了 Deref<Target=U>,那 &T 在传给需要 &U 的函数时会自动转换。经典例子:String 实现了 Deref<Target=str>,所以 &String 能自动变成 &str:
fn display(s: &str) {
println!("{}", s);
}
fn main() {
let s = String::from("hello");
display(&s); // &String 自动 Deref 成 &str
}
而且这种转换能连续进行。比如 MyBox<String>:先 Deref 成 String,发现还不够,再 Deref 成 &str,直到匹配上参数类型。完全在编译期完成,零运行时开销。
Tip这就是为什么你写
&String、甚至&Vec<...>传给要&str或切片的函数时从不出错——背后是编译器的 Deref 链在默默工作。看不懂某段”类型对不上却编过了”的代码,多半是 Deref 在帮忙。
还有个底层的”引用归一化”:编译器只会对 &v 形式解引用,遇到 &&&&v 或多重智能指针,会先归一化成 &v 再解。所以 &&Foo 也能直接调 Foo 的方法,层层脱壳直到拿到真正的 &Foo。
1-4 DerefMut 与三种转换
除了不可变转换,Rust 还支持可变转换,规则有三条:
T: Deref<Target=U>:&T→&UT: DerefMut<Target=U>:&mut T→&mut UT: Deref<Target=U>:&mut T→&U(可变引用能转成不可变引用)
use std::ops::{Deref, DerefMut};
struct MyBox<T>(T);
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T { &self.0 }
}
impl<T> DerefMut for MyBox<T> {
fn deref_mut(&mut self) -> &mut T { &mut self.0 }
}
注意实现 DerefMut 必须先实现 Deref。第三条规则单向成立:可变引用能降级成不可变,反之不行——这正好符合借用规则(可变借用是唯一的,降级成共享借用不破坏规则;但共享借用在场时绝不允许冒出可变借用)。
1-5 Drop:离开作用域自动清理
Drop 是智能指针的另一个标配,定义值离开作用域时自动执行的收尾代码。正因为它,Rust 能同时甩开 GC 和手动 free:编译器在你变量失效处自动插入 drop 调用。
struct HasDrop;
impl Drop for HasDrop {
fn drop(&mut self) {
println!("HasDrop 被释放了");
}
}
fn main() {
let _x = HasDrop;
println!("运行中...");
}
输出先 “运行中…”,再 “HasDrop 被释放了”——说明 drop 在 main 结束时才跑。
Drop 的顺序有规律:
- 变量级别逆序:先创建的后释放。
- 结构体内部顺序:字段按定义顺序依次 drop。
Note即使你不为某类型实现
Drop,Rust 也会为几乎所有类型提供默认 drop——它挨个调用每个字段的 drop。所以结构体里哪怕只装普通字段,离开作用域也会被自动清理。
1-6 不能手动调 drop 方法,要用函数
想提前释放怎么办?你可能会写 foo.drop(),但编译器直接拒绝:explicit destructor calls not allowed。原因是 Drop::drop 只借用了 &mut self,如果允许你手动调,后面代码还能继续用它,等于访问一个正在被清理的值,极其危险。
正确姿势是用 std::mem::drop 函数(它在 prelude 里,无需 use):
fn main() {
let foo = HasDrop;
drop(foo); // 拿走 foo 的所有权,离开这行作用域即触发真正 drop
// println!("{:?}", foo); // 报错:foo 所有权已转移
}
drop 函数签名是 pub fn drop<T>(_x: T),它拿走所有权。函数结束时 _x 离开作用域,编译器才执行真正的析构。所以 drop(foo) 本质是”让 foo 提前走出作用域”。
1-7 Drop 的常见用途与限制
Drop 主要做两件事:回收内存、执行收尾(关文件、断连接、解锁)。绝大多数情况你不用手动 drop,Rust 自动搞定,连复杂类型的每个字段都会单独 drop。只有少数场景(文件描述符、网络 socket、锁)需要你在 Drop 里显式关闭资源。
还有个铁律:一个类型不能同时实现 Copy 和 Drop。Copy 类型会被编译器隐式复制,析构时机无法预测,所以禁止拥有析构函数。写了 #[derive(Copy)] 又 impl Drop 会直接编译报错。
Warning初学者常误以为
Drop能”防止内存泄漏”。其实它只是保证资源在离开作用域时被清理——如果值因为引用循环永远走不出作用域(后面章节会讲),Drop 也就永远没机会跑。Drop 管”到点的清理”,管不了”到点不了”的情况。
1-8 Deref 与 Drop 的使用提醒
这两个 trait 看着简单,使用里有些细节值得记牢。
关于 Deref:你几乎从不需要给自己写的普通类型实现 Deref。官方建议只在”自定义智能指针”上实现它。如果你只是想把某个字段方便地取出来,别用 Deref 取巧——那会掩盖真实的类型关系,让读者困惑”这到底是个啥”。Deref 的价值在于让智能指针和引用无缝混用,不是用来省写一行访问器。
关于 Drop:它的 drop 方法拿到的是 &mut self,不是所有权。这意味着你不能在 drop 里把字段”移走”——因为数据随后还要被编译器真正释放,你不能提前掏空它。想在 drop 时拿走某个字段的值,用 std::mem::take 之类换成默认值,而不是移动。
Warning绝不要在
drop里又去调用别的会 panic 的逻辑、或形成再次触发 drop 的循环。Drop 期间 panic 会导致程序直接中止(abort),比普通 panic 更难处理。保持drop简短、只做收尾(关文件、释放句柄),别在里面写复杂业务。
还有个易混点:Drop 和”析构”在不同语言里叫法不同,但 Rust 里就是它。只要你的值走出作用域,无论是正常结束、还是 early return、还是所在函数 panic 了,Drop 都会跑——这保证了资源不泄漏。理解这一点,你就理解了为什么 Rust 敢说”大部分时候不用手动管理资源”。
下一章我们看引用计数指针 Rc<T>,它让一个数据可以有多个主人。