变量离开作用域时会发生什么?

解读

在国内 Rust 社招/校招面试中,这道题出现的频率极高,面试官真正想考察的是:

  1. 候选人是否理解 所有权系统(Ownership) 的“资源即对象”思想;
  2. 能否把 Drop trait栈展开(stack unwinding) 联系起来,说明内存、锁、文件句柄等资源如何被确定性释放
  3. 是否知道 移动语义(Move)复制语义(Copy) 在离开作用域时的差异;
  4. 能否举出循环引用(Rc<T> + RefCell<T>自引用结构体导致的Drop 未触发二次释放隐患,并给出标准库或第三方 crate 的解决方案。
    回答时切忌只背“调用 drop”,而要展示对编译期插入的 drop gluepanic 安全unsafe 代码中的手动 drop 等实战细节的掌握。

知识点

  1. Ownership 三规则:每个值有唯一所有者;同一时刻只能有一个可变引用;所有者离开作用域时值被丢弃。
  2. Drop trait:编译器在** MIR 生成阶段为每个变量插入 drop glue,按逆序**执行;自定义类型可实现 Drop::drop(&mut self) 做清理。
  3. Copy vs Move:实现 Copy 的类型在离开作用域时按位复制,不调用 drop;未实现 Copy 的类型发生移动(move),原变量变为未初始化状态,不再 drop。
  4. 栈展开:panic 时 Rust 按栈帧逆序调用 drop,保证异常安全(exception safety);若 drop 本身 panic,则触发 double panic -> abort
  5. RAII 包装器Vec<T>MutexGuardFileBufWriter 等利用 drop 做自动释放/解锁/刷盘;面试常追问“如何防止忘记 unlock”即可答“用 RAII,离开作用域自动 drop”。
  6. 手动干预std::mem::drop 只是提前调用,本质仍是 move 进函数再触发 drop;std::mem::forget 泄漏资源,不再调用 drop,需配合 unsafe { ManuallyDrop::new } 使用。
  7. 循环引用与弱引用Rc<T> + RefCell<T> 形成循环时引用计数永不为 0,drop 不触发;需用 Weak<T> 打破循环或采用 unsafe 的 Weak::into_raw + from_raw 手动清理。
  8. 自引用结构体pin-projectouroboros 保证固定内存地址,否则 move 后 drop 会访问失效指针;面试可提“用 Pin<Box<T>> + 投影”解决。
  9. unsafe 场景Box::from_raw 后必须手动 drop,否则内存泄漏;Vec::set_len 前需 std::ptr::drop_in_place 截断部分元素。
  10. 零成本抽象:drop glue 在编译期单态化,无运行时虚函数开销;#[inline] 提示可进一步消除函数调用。

答案

当变量离开作用域时,Rust 编译器会按定义逆序为其生成 drop glue

  1. 若变量实现了 Drop,则调用 Drop::drop(&mut self)
  2. 若变量是元组或结构体,则递归对其所有字段执行 drop;
  3. 若变量是移动后剩余的空壳(已发生 move),则不再 drop
  4. 若变量是Copy 类型(如 i32、&T),则无 drop 逻辑,直接丢弃内存;
  5. 在 panic 发生栈展开时,drop 仍会被保证调用,形成异常安全
  6. 开发者可用 std::mem::drop 提前触发,也可用 std::mem::forget 故意泄漏
  7. 对于 unsafe 代码手动分配的内存,需自行调用 drop_in_placeBox::from_raw 配套释放,否则产生内存泄漏double free
    一句话总结:离开作用域即调用 drop,这是 Rust 无 GC 却能做到内存安全与资源自动回收的核心机制。

拓展思考

  1. 如果在一个 #[no_std] 嵌入式环境里,全局静态 Mutex<T> 的 drop 何时执行?——静态变量生命周期为整个程序,drop 在进程退出时由 runtime 或 OS 回收,嵌入式常禁止全局析构,需用 #[used] + link_sectioncortex-m-rt.pre_init 手动清零。
  2. async 状态机pin 后,生成的 Future 可能跨 await 点存活,drop 会在状态机销毁时统一清理;若内部持有 MutexGuard,可能阻塞整个 executor,面试可追问“如何设计异步锁”——答 tokio::sync::Mutex 把 guard 做成 Send + 'static,drop 时异步释放。
  3. FFI 场景中,把 Box<T> 指针传给 C 后,必须在 C 侧提供 extern "C" fn release(ptr: *mut T),内部做 Box::from_raw 触发 drop,否则 Rust 侧无法感知生命周期,造成内存泄漏
  4. 当结构体里出现 ManuallyDrop<T> 字段时,drop glue 不会自动递归,需要在 Drop::drop 中手动调用 ManuallyDrop::dropstd::ptr::drop_in_place,否则泄漏资源;这也是 Pin + Future 内部常见的自引用清理技巧