首页 / Rust 入门教程 / Deref 与 Drop trait

Rust 入门教程

Deref 与 Drop trait

本教程共 78 篇 · 第 61 篇 · 更新于 2026-08-08 · 约 10 分钟阅读

RustRust 入门教程DerefDrop智能指针解引用deref_mut所有权

本节目标:理解 Deref 如何让智能指针像普通引用一样用 * 解引用、并在函数传参时发生隐式转换;理解 Drop 如何定义值离开作用域时的自动清理,以及为什么不能手动调用 drop 方法而要用 std::mem::drop 函数。

上章说”智能指针”之所以智能,关键在它实现了两个 trait:DerefDrop。有了它们,智能指针用起来和引用几乎没区别,还能在离开作用域时自动收尾。这章把这两个 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 方法返回的是常规引用 &Ttype 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&U
  • T: DerefMut<Target=U>&mut T&mut U
  • T: 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 被释放了”——说明 dropmain 结束时才跑。

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 里显式关闭资源。

还有个铁律:一个类型不能同时实现 CopyDropCopy 类型会被编译器隐式复制,析构时机无法预测,所以禁止拥有析构函数。写了 #[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>,它让一个数据可以有多个主人。