变量离开作用域时会发生什么?
解读
在国内 Rust 社招/校招面试中,这道题出现的频率极高,面试官真正想考察的是:
- 候选人是否理解 所有权系统(Ownership) 的“资源即对象”思想;
- 能否把 Drop trait 与 栈展开(stack unwinding) 联系起来,说明内存、锁、文件句柄等资源如何被确定性释放;
- 是否知道 移动语义(Move) 与 复制语义(Copy) 在离开作用域时的差异;
- 能否举出循环引用(Rc<T> + RefCell<T>)或自引用结构体导致的Drop 未触发或二次释放隐患,并给出标准库或第三方 crate 的解决方案。
回答时切忌只背“调用 drop”,而要展示对编译期插入的 drop glue、panic 安全、unsafe 代码中的手动 drop 等实战细节的掌握。
知识点
- Ownership 三规则:每个值有唯一所有者;同一时刻只能有一个可变引用;所有者离开作用域时值被丢弃。
- Drop trait:编译器在** MIR 生成阶段为每个变量插入 drop glue,按逆序**执行;自定义类型可实现
Drop::drop(&mut self)
做清理。 - Copy vs Move:实现
Copy
的类型在离开作用域时按位复制,不调用 drop;未实现Copy
的类型发生移动(move),原变量变为未初始化状态,不再 drop。 - 栈展开:panic 时 Rust 按栈帧逆序调用 drop,保证异常安全(exception safety);若 drop 本身 panic,则触发 double panic -> abort。
- RAII 包装器:
Vec<T>
、MutexGuard
、File
、BufWriter
等利用 drop 做自动释放/解锁/刷盘;面试常追问“如何防止忘记 unlock”即可答“用 RAII,离开作用域自动 drop”。 - 手动干预:
std::mem::drop
只是提前调用,本质仍是 move 进函数再触发 drop;std::mem::forget
泄漏资源,不再调用 drop,需配合 unsafe { ManuallyDrop::new } 使用。 - 循环引用与弱引用:
Rc<T>
+RefCell<T>
形成循环时引用计数永不为 0,drop 不触发;需用Weak<T>
打破循环或采用 unsafe 的 Weak::into_raw + from_raw 手动清理。 - 自引用结构体:
pin-project
或ouroboros
保证固定内存地址,否则 move 后 drop 会访问失效指针;面试可提“用 Pin<Box<T>> + 投影”解决。 - unsafe 场景:
Box::from_raw
后必须手动 drop,否则内存泄漏;Vec::set_len
前需std::ptr::drop_in_place
截断部分元素。 - 零成本抽象:drop glue 在编译期单态化,无运行时虚函数开销;#[inline] 提示可进一步消除函数调用。
答案
当变量离开作用域时,Rust 编译器会按定义逆序为其生成 drop glue:
- 若变量实现了
Drop
,则调用Drop::drop(&mut self)
; - 若变量是元组或结构体,则递归对其所有字段执行 drop;
- 若变量是移动后剩余的空壳(已发生 move),则不再 drop;
- 若变量是Copy 类型(如 i32、&T),则无 drop 逻辑,直接丢弃内存;
- 在 panic 发生栈展开时,drop 仍会被保证调用,形成异常安全;
- 开发者可用
std::mem::drop
提前触发,也可用std::mem::forget
故意泄漏; - 对于 unsafe 代码手动分配的内存,需自行调用
drop_in_place
或Box::from_raw
配套释放,否则产生内存泄漏或double free。
一句话总结:离开作用域即调用 drop,这是 Rust 无 GC 却能做到内存安全与资源自动回收的核心机制。
拓展思考
- 如果在一个 #[no_std] 嵌入式环境里,全局静态
Mutex<T>
的 drop 何时执行?——静态变量生命周期为整个程序,drop 在进程退出时由 runtime 或 OS 回收,嵌入式常禁止全局析构,需用#[used] + link_section
或cortex-m-rt
的.pre_init
手动清零。 - async 状态机被
pin
后,生成的Future
可能跨 await 点存活,drop 会在状态机销毁时统一清理;若内部持有 MutexGuard,可能阻塞整个 executor,面试可追问“如何设计异步锁”——答 tokio::sync::Mutex 把 guard 做成 Send + 'static,drop 时异步释放。 - FFI 场景中,把
Box<T>
指针传给 C 后,必须在 C 侧提供 extern "C" fn release(ptr: *mut T),内部做Box::from_raw
触发 drop,否则 Rust 侧无法感知生命周期,造成内存泄漏。 - 当结构体里出现 ManuallyDrop<T> 字段时,drop glue 不会自动递归,需要在 Drop::drop 中手动调用 ManuallyDrop::drop 或
std::ptr::drop_in_place
,否则泄漏资源;这也是 Pin + Future 内部常见的自引用清理技巧。