前面讲过 Drop 的第一层作用:值离开作用域时自动释放资源。
这已经能替代很多 C 里的 goto cleanup。但真正在库代码里包装文件句柄、连接、锁、临时目录或 FFI handle 时,只知道“离开作用域会释放”还不够。
C 程序员还会关心这些问题:
- 释放顺序到底是什么
- 能不能提前释放
- 析构失败怎么办
- 结构体字段谁先释放
Drop里能不能把字段移出来- FFI handle 应该怎样包成 Rust 类型
这些才是 Drop 在工程里容易踩到的地方。
Drop 自动运行,不是手工 cleanup 函数
Rust 类型可以实现 Drop:
struct Session {
id: u32,
}
impl Drop for Session {
fn drop(&mut self) {
println!("close session {}", self.id);
}
}
当 Session 的值离开作用域,drop 自动运行:
fn main() {
let session = Session { id: 1 };
println!("work");
} // session 在这里 drop
不要把 drop 方法当成 C 里的 session_close(&session) 来手工调用:
let mut session = Session { id: 1 };
// session.drop(); // 不允许
Rust 不让你直接调用 Drop::drop,因为那样会留下一个“已经清理过但变量还在”的值。C 里这正是 use-after-free 和 double free 的来源。
需要提前释放时,用标准库函数 drop(value):
let session = Session { id: 1 };
drop(session);
// session 不能再使用
drop(session) 会消耗这个值。提前释放和所有权系统绑定在一起,释放后变量不能再访问。
局部变量按反向顺序释放
C 的 cleanup 通常会手工安排释放顺序:
cleanup:
close_child(child);
close_parent(parent);
Rust 里,同一作用域中的局部变量通常按创建的相反顺序 drop:
struct Resource(&'static str);
impl Drop for Resource {
fn drop(&mut self) {
println!("drop {}", self.0);
}
}
fn main() {
let parent = Resource("parent");
let child = Resource("child");
}
输出顺序是:
drop child
drop parent
这很符合资源依赖:后创建的临时资源,通常先释放。
如果你需要更早释放某个资源,使用更小的作用域:
fn main() {
let parent = Resource("parent");
{
let child = Resource("child");
println!("use child");
} // child 在这里 drop
println!("parent still alive");
}
作用域不是语法装饰,而是资源生命周期的一部分。
结构体字段按声明顺序释放
局部变量是反向顺序,但结构体字段不是看初始化顺序,而是按字段声明顺序 drop。
struct Resource(&'static str);
impl Drop for Resource {
fn drop(&mut self) {
println!("drop {}", self.0);
}
}
struct Pair {
first: Resource,
second: Resource,
}
fn main() {
let _pair = Pair {
first: Resource("first"),
second: Resource("second"),
};
}
字段会按声明顺序释放:
drop first
drop second
这点和局部变量的“反向顺序”不一样。写资源包装类型时,如果字段之间有释放依赖,不要靠“我初始化时写在前面还是后面”猜顺序。
更稳妥的做法是把依赖关系放进一个字段里,或者在 Drop 中显式处理真正需要先做的动作。
Drop 不能返回错误
C 里释放函数可能有返回值:
int session_close(struct session *s);
Rust 的 Drop::drop 签名固定是:
fn drop(&mut self)
它不能返回 Result。这不是疏漏,而是因为 drop 可能发生在很多隐式路径上:作用域结束、错误提前返回、panic 展开、容器释放元素。调用方没有一个自然的位置处理 drop 失败。
所以需要报告错误的收尾动作,应该设计成显式方法:
struct Writer {
file: std::fs::File,
}
impl Writer {
fn finish(mut self) -> std::io::Result<()> {
use std::io::Write;
self.file.flush()?;
self.file.sync_all()?;
Ok(())
}
}
Drop 仍然可以负责“尽量释放句柄”。但像 flush、commit、sync_all 这种影响业务语义的动作,要让调用方显式处理错误。
一个实用规则:
释放资源 -> Drop
需要知道是否成功 -> 显式方法返回 Result
Drop 里不能随便移出字段
drop(&mut self) 拿到的是 &mut self,不是 self。这意味着你不能直接把拥有所有权的字段移出去:
struct Holder {
data: String,
}
impl Drop for Holder {
fn drop(&mut self) {
// let data = self.data; // 不允许
}
}
原因很直接:drop 返回后,Rust 还要继续按规则清理这个值的字段。如果你把字段移走,结构体会变成部分无效状态。
如果确实需要在 Drop 中取走某个字段,常见写法是用 Option<T> 包一层:
struct Holder {
data: Option<String>,
}
impl Drop for Holder {
fn drop(&mut self) {
if let Some(data) = self.data.take() {
println!("drop with data: {data}");
}
}
}
take() 会把字段里的值拿出来,并留下 None。这样结构体仍然处于有效状态。
不过不要滥用这个模式。大多数 Drop 只需要借用字段做清理,不需要移动字段。
用 Drop 包装 C handle
写 FFI 或系统库时,Drop 最常见的用途是包装 C handle。
C API 可能长这样:
struct device *device_open(const char *path);
void device_close(struct device *dev);
Rust 侧可以把裸指针包进一个拥有类型:
use std::ptr::NonNull;
struct Device {
raw: NonNull<ffi::device>,
}
impl Drop for Device {
fn drop(&mut self) {
unsafe {
ffi::device_close(self.raw.as_ptr());
}
}
}
这个 Device 的语义很清楚:只要你拥有 Device 值,就拥有底层 C handle;Device 离开作用域时,自动调用 device_close。
构造函数负责把 C 的错误情况转换成 Rust 的 Result:
impl Device {
fn open(path: &std::ffi::CStr) -> Result<Self, OpenError> {
let raw = unsafe { ffi::device_open(path.as_ptr()) };
let raw = NonNull::new(raw).ok_or(OpenError)?;
Ok(Self { raw })
}
}
这样,裸指针和 unsafe 被限制在包装类型内部。外部用户只拿到一个会自动释放的 Rust 值。
mem::forget 会故意不释放
Rust 也有办法故意不运行 Drop:
let session = Session { id: 1 };
std::mem::forget(session);
forget 会消耗值,但不调用它的 Drop。这会泄漏资源。
普通业务代码几乎不需要它。它主要出现在 FFI 或非常底层的所有权转交场景:你把资源交给了 C,Rust 这边就不能再自动释放。
如果只是想延迟释放、调整释放顺序或提前释放,不要用 forget。用作用域或 drop(value)。
这一篇先记住什么
Drop 是 Rust 的资源收尾机制,但它不是 C cleanup 函数的简单换名。
几个工程规则更重要:
不要手工调用 Drop::drop,用 drop(value) 提前释放
局部变量通常按创建的相反顺序释放
结构体字段按声明顺序释放
Drop 不能返回错误,需要报告结果就设计 finish/close/commit 方法
包装 C handle 时,让拥有类型实现 Drop,把 unsafe 限制在边界
不要用 mem::forget 逃避普通释放问题
C 里清理路径靠约定和代码审查守住。Rust 的做法是把“谁拥有、何时释放、释放后不能再用”变成类型系统能检查的规则。
下一篇进入 match 和模式匹配。前面已经讲过 enum 能替代 tagged union,接下来要看 Rust 怎样把状态分支和数据解包合在一起。