跳转到主内容资源释放:从 cleanup 到 Drop | 物联网民工

资源释放:从 cleanup 到 Drop

C 代码里,资源释放通常是靠人维护的。

打开文件要 fclose,申请内存要 free,拿到锁要解锁,初始化了对象就要在每条错误路径里销毁。函数短时还好,资源一多,最常见的结构就是 goto cleanup。

Rust 没有取消资源释放。它把释放责任和“谁拥有资源”绑定起来:值离开作用域时,自动运行清理逻辑。这个机制叫 Drop。

这一篇先不讲复杂生命周期,只看 C 开发者最关心的工程问题:多条返回路径里,资源怎样不漏掉。

C 里 cleanup 是必要的

看一个常见 C 结构:

#include <stdio.h>
#include <stdlib.h>

int copy_file(const char *src, const char *dst)
{
    FILE *in = NULL;
    FILE *out = NULL;
    char *buf = NULL;
    int ret = -1;

    in = fopen(src, "rb");
    if (in == NULL) {
        goto cleanup;
    }

    out = fopen(dst, "wb");
    if (out == NULL) {
        goto cleanup;
    }

    buf = malloc(4096);
    if (buf == NULL) {
        goto cleanup;
    }

    /* read and write ... */
    ret = 0;

cleanup:
    free(buf);
    if (out != NULL) {
        fclose(out);
    }
    if (in != NULL) {
        fclose(in);
    }
    return ret;
}

这不是坏代码。对 C 来说,它反而是一种成熟写法:把释放路径集中起来,避免每个错误分支都手写一遍 free / fclose。

问题在于,资源释放仍然靠人工维护:

  • 新增资源时要记得加到 cleanup
  • cleanup 顺序要自己确认
  • 中途提前 return 可能绕过释放
  • 所有权转移后,原路径不能再释放
  • 多个函数之间传递资源时,谁负责释放要靠约定

Rust 的 Drop 解决的不是“有没有释放”,而是让释放责任进入类型和作用域。

Rust 里资源跟着值走

Rust 版本可以直接把资源放进值里:

use std::fs::File;
use std::io::{self, copy};

fn copy_file(src: &str, dst: &str) -> io::Result<()> {
    let mut input = File::open(src)?;
    let mut output = File::create(dst)?;

    copy(&mut input, &mut output)?;
    Ok(())
}

这里没有显式 close。

File 是一个拥有文件描述符的值。input 和 output 离开作用域时,Rust 会自动调用它们的清理逻辑,关闭底层文件。

即使中间的 ? 返回错误,已经创建成功的值也会按作用域退出时的规则释放。

File::open 成功 -> input 拥有文件
File::create 失败 -> input 离开作用域 -> 自动关闭
copy 失败 -> input/output 离开作用域 -> 自动关闭
函数成功返回 -> input/output 离开作用域 -> 自动关闭

这就是 C 里 cleanup 想达到的效果,只是 Rust 把它放进了类型和作用域。

Drop 像析构,但不要手动调用

Rust 里类型可以实现 Drop:

struct Session {
    id: u32,
}

impl Drop for Session {
    fn drop(&mut self) {
        println!("close session {}", self.id);
    }
}

fn main() {
    let session = Session { id: 1 };
    println!("work");
}

main 结束时,session 离开作用域,drop 会自动运行。

可以把它类比成 C++ 的析构函数,但对 C 开发者来说,更有用的理解是:

Drop = 这个值离开作用域时要执行的清理动作

不要把 drop 当成普通清理函数手动调用。需要提前释放时,用标准库函数 drop(value),它会消耗这个值:

let file = File::open("data.bin")?;
drop(file); // 从这里开始 file 不能再用

这点很重要。提前释放以后,值已经被移动进 drop,后面不能再访问,避免了 C 里常见的 use-after-free。

所有权转移后,释放责任也转移

C 里传递资源时,最容易出错的是所有权约定。

void take_buffer(uint8_t *buf);  /* 这个函数会不会 free buf? */

如果文档没写清,调用方很容易重复释放或漏释放。

Rust 里,如果函数接收拥有所有权的值,释放责任会跟着移动:

fn consume_buffer(buf: Vec<u8>) {
    println!("len = {}", buf.len());
}

fn main() {
    let data = vec![1, 2, 3];
    consume_buffer(data);
    // data 不能再使用
}

data 被传给 consume_buffer 后,所有权移动进函数。函数结束时,buf 离开作用域,内存释放。

如果函数只是借用,不拿走所有权,就接收 slice:

fn inspect_buffer(buf: &[u8]) {
    println!("len = {}", buf.len());
}

fn main() {
    let data = vec![1, 2, 3];
    inspect_buffer(&data);
    println!("{}", data.len()); // 还能继续用
}

这就是前面讲 Vec<u8> 和 &[u8] 时埋下的区别:Vec 表示拥有数据,&[u8] 表示借用数据。

释放顺序由作用域决定

Rust 的局部变量通常按创建的相反顺序释放:

struct Resource(&'static str);

impl Drop for Resource {
    fn drop(&mut self) {
        println!("drop {}", self.0);
    }
}

fn main() {
    let a = Resource("a");
    let b = Resource("b");
}

作用域结束时,b 先释放,a 后释放。

这和 C cleanup 里手写释放顺序类似,但 Rust 会按作用域规则自动执行。需要更精确的释放顺序时,可以引入更小的作用域:

{
    let temp = Resource("temp");
    println!("use temp");
} // temp 在这里释放

这种写法在处理临时文件、锁、临时 buffer 时很常见。作用域本身就是资源生命周期的一部分。

Drop 不能替代显式错误处理

有一点需要避免误解:Drop 适合释放资源,但不适合承载需要报告的业务错误。

例如文件关闭时可能失败。Rust 的 File 在 drop 时会关闭文件,但如果你需要确认数据已经落盘,不能只依赖离开作用域。应该显式调用:

use std::io::Write;

fn write_config(mut file: std::fs::File) -> std::io::Result<()> {
    file.write_all(b"config")?;
    file.sync_all()?;
    Ok(())
}

Drop 的定位是“尽量把资源收掉”。如果清理结果会影响业务语义,就应该用普通函数显式返回 Result。

这和 C 里 fclose、fsync、close 的区别类似:释放句柄是一回事,确认数据持久化是另一回事。

这一篇先记住什么

C 和 Rust 的第一层对应关系可以这样看:

malloc/free              -> Vec / Box 等拥有资源的值 + Drop
fopen/fclose             -> File + Drop
goto cleanup             -> 作用域退出自动 drop
提前释放                 -> drop(value)
谁负责释放               -> 谁拥有这个值
函数只是读取             -> 借用,如 &[u8] / &str
函数接管资源             -> 接收拥有所有权的值,如 Vec<u8>

Drop 不是让资源不用释放,而是把释放时机绑定到值的生命周期。

C 里靠 cleanup 维护的规则,在 Rust 里会更早进入类型:谁拥有资源,谁负责释放;只是临时使用,就借用。

下一篇可以进入所有权本身。前面已经看到它在 buffer、字符串、错误和资源释放里反复出现,现在可以单独讲“谁拥有,谁释放,移动以后为什么不能再用”。