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、字符串、错误和资源释放里反复出现,现在可以单独讲“谁拥有,谁释放,移动以后为什么不能再用”。