跳转到主内容Drop 深入 | 物联网民工

Drop 深入

前面讲过 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 怎样把状态分支和数据解包合在一起。