析构函数

当一个已初始化变量临时值离开作用域时,其 析构函数 会运行,或者说它被 丢弃(dropped) 了。赋值操作也会运行其左操作数的析构函数(如果它已被初始化)。如果一个变量是部分初始化的,则只丢弃其已初始化的字段。

类型 T 的析构函数包含

  1. 如果 T: Drop,则调用<T as std::ops::Drop>::drop
  2. 递归地运行其所有字段的析构函数。
    • 结构体的字段按照声明顺序丢弃。
    • 枚举变体的活动字段按照声明顺序丢弃。
    • 元组的字段按照顺序丢弃。
    • 数组或所有权切片的元素从第一个到最后一个丢弃。
    • 闭包通过 move 捕获的变量按照未指定的顺序丢弃。
    • Trait 对象运行底层类型的析构函数。
    • 其他类型不会导致进一步的丢弃。

如果析构函数必须手动运行,例如在实现自己的智能指针时,可以使用std::ptr::drop_in_place

一些例子

#![allow(unused)]
fn main() {
struct PrintOnDrop(&'static str);

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

let mut overwritten = PrintOnDrop("drops when overwritten");
overwritten = PrintOnDrop("drops when scope ends");

let tuple = (PrintOnDrop("Tuple first"), PrintOnDrop("Tuple second"));

let moved;
// No destructor run on assignment.
moved = PrintOnDrop("Drops when moved");
// Drops now, but is then uninitialized.
moved;

// Uninitialized does not drop.
let uninitialized: PrintOnDrop;

// After a partial move, only the remaining fields are dropped.
let mut partial_move = (PrintOnDrop("first"), PrintOnDrop("forgotten"));
// Perform a partial move, leaving only `partial_move.0` initialized.
core::mem::forget(partial_move.1);
// When partial_move's scope ends, only the first field is dropped.
}

丢弃作用域(Drop scopes)

每个变量或临时值都关联到一个 丢弃作用域(drop scope)。当控制流离开一个丢弃作用域时,所有关联到该作用域的变量按照声明顺序(对于变量)或创建顺序(对于临时值)的逆序被丢弃。

丢弃作用域是在将 forif letwhile let 表达式替换为其使用 match 的等价表达式之后确定的。

重载运算符与内置运算符没有区别,并且不考虑绑定模式

给定一个函数或闭包,有以下几种丢弃作用域:

  • 整个函数
  • 每个块,包括函数体
    • 对于块表达式,块的作用域和表达式的作用域是同一个作用域。
  • match 表达式的每个分支(arm)

丢弃作用域之间存在以下嵌套关系。当同时离开多个作用域时,例如函数返回时,变量从内向外丢弃。

  • 整个函数作用域是最外层的作用域。
  • 函数体块包含在整个函数的作用域内。
  • 表达式语句中表达式的父作用域是该语句的作用域。
  • let 语句初始化表达式的父作用域是该 let 语句的作用域。
  • 语句作用域的父作用域是包含该语句的块的作用域。
  • match 守卫表达式的父作用域是该守卫所属分支(arm)的作用域。
  • match 表达式中 => 之后表达式的父作用域是该表达式所在分支(arm)的作用域。
  • 分支(arm)作用域的父作用域是该分支所属的 match 表达式的作用域。
  • 所有其他作用域的父作用域是其紧邻的包含表达式的作用域。

函数参数的作用域

所有函数参数都在整个函数体作用域内,因此在评估函数时最后丢弃。每个实际的函数参数在其模式中引入的任何绑定之后丢弃。

#![allow(unused)]
fn main() {
struct PrintOnDrop(&'static str);
impl Drop for PrintOnDrop {
    fn drop(&mut self) {
        println!("drop({})", self.0);
    }
}
// Drops `y`, then the second parameter, then `x`, then the first parameter
fn patterns_in_parameters(
    (x, _): (PrintOnDrop, PrintOnDrop),
    (_, y): (PrintOnDrop, PrintOnDrop),
) {}

// drop order is 3 2 0 1
patterns_in_parameters(
    (PrintOnDrop("0"), PrintOnDrop("1")),
    (PrintOnDrop("2"), PrintOnDrop("3")),
);
}

局部变量的作用域

let 语句中声明的局部变量关联到包含该 let 语句的块的作用域。在 match 表达式中声明的局部变量关联到它们被声明所在的 match 分支的作用域。

#![allow(unused)]
fn main() {
struct PrintOnDrop(&'static str);
impl Drop for PrintOnDrop {
    fn drop(&mut self) {
        println!("drop({})", self.0);
    }
}
let declared_first = PrintOnDrop("Dropped last in outer scope");
{
    let declared_in_block = PrintOnDrop("Dropped in inner scope");
}
let declared_last = PrintOnDrop("Dropped first in outer scope");
}

如果在同一个分支(arm)中使用了多个模式用于 match 表达式,则将使用未指定的模式来确定丢弃顺序。

临时作用域(Temporary scopes)

表达式的 临时作用域 是当表达式用于place context时,用于保存该表达式结果的临时变量的作用域,除非它被常量提升了。

除了生命周期延长之外,表达式的临时作用域是包含该表达式的最小作用域,并且是以下之一:

注意

match 表达式的检查表达式(scrutinee)不是临时作用域,因此检查表达式中的临时值可以在 match 表达式之后丢弃。例如,match 1 { ref mut z => z };1 的临时值会一直存活到语句结束。

版本差异:2024 版本增加了两个新的临时作用域缩小规则:if let 的临时值在 else 块之前丢弃,块的尾部表达式的临时值在尾部表达式评估后立即丢弃。

一些例子

#![allow(unused)]
fn main() {
struct PrintOnDrop(&'static str);
impl Drop for PrintOnDrop {
    fn drop(&mut self) {
        println!("drop({})", self.0);
    }
}
let local_var = PrintOnDrop("local var");

// Dropped once the condition has been evaluated
if PrintOnDrop("If condition").0 == "If condition" {
    // Dropped at the end of the block
    PrintOnDrop("If body").0
} else {
    unreachable!()
};

if let "if let scrutinee" = PrintOnDrop("if let scrutinee").0 {
    PrintOnDrop("if let consequent").0
    // `if let consequent` dropped here
}
// `if let scrutinee` is dropped here
else {
    PrintOnDrop("if let else").0
    // `if let else` dropped here
};

// Dropped before the first ||
(PrintOnDrop("first operand").0 == ""
// Dropped before the )
|| PrintOnDrop("second operand").0 == "")
// Dropped before the ;
|| PrintOnDrop("third operand").0 == "";

// Scrutinee is dropped at the end of the function, before local variables
// (because this is the tail expression of the function body block).
match PrintOnDrop("Matched value in final expression") {
    // Dropped once the condition has been evaluated
    _ if PrintOnDrop("guard condition").0 == "" => (),
    _ => (),
}
}

操作数

在评估其他操作数时,也会创建临时值来保存表达式操作数的结果。这些临时值关联到包含该操作数的表达式的作用域。由于一旦表达式评估完成,这些临时值就会被 move 走,所以丢弃它们没有效果,除非表达式的一个操作数跳出表达式、返回或发生恐慌(panic)

#![allow(unused)]
fn main() {
struct PrintOnDrop(&'static str);
impl Drop for PrintOnDrop {
    fn drop(&mut self) {
        println!("drop({})", self.0);
    }
}
loop {
    // Tuple expression doesn't finish evaluating so operands drop in reverse order
    (
        PrintOnDrop("Outer tuple first"),
        PrintOnDrop("Outer tuple second"),
        (
            PrintOnDrop("Inner tuple first"),
            PrintOnDrop("Inner tuple second"),
            break,
        ),
        PrintOnDrop("Never created"),
    );
}
}

常量提升(Constant promotion)

值表达式提升到 'static 位置发生于当表达式可以写在常量中并被借用,并且该借用可以在表达式最初被写的位置被解引用,而不会改变运行时行为时。也就是说,提升的表达式可以在编译时评估,并且结果值不包含内部可变性(interior mutability)析构函数(destructors)(这些属性在可能的情况下基于值确定,例如 &None 总是具有类型 &'static Option<_>,因为它不包含任何不允许的内容)。

临时生命周期延长(Temporary lifetime extension)

注意

临时生命周期延长的具体规则可能会发生变化。这里仅描述当前的行为。

let 语句中表达式的临时作用域有时会被 延长 到包含该 let 语句的块的作用域。这是在通常的临时作用域过小,基于某些语法规则时进行的。例如:

#![allow(unused)]
fn main() {
let x = &mut 0;
// Usually a temporary would be dropped by now, but the temporary for `0` lives
// to the end of the block.
println!("{}", x);
}

生命周期延长也适用于 staticconst 项,这使得临时值一直存活到程序结束。例如:

#![allow(unused)]
fn main() {
const C: &Vec<i32> = &Vec::new();
// Usually this would be a dangling reference as the `Vec` would only
// exist inside the initializer expression of `C`, but instead the
// borrow gets lifetime-extended so it effectively has `'static` lifetime.
println!("{:?}", C);
}

如果借用解引用字段访问元组索引表达式具有延长的临时作用域,则其操作数也具有延长的临时作用域。如果索引表达式具有延长的临时作用域,则被索引的表达式也具有延长的临时作用域。

基于模式的延长

延长模式(extending pattern) 是指以下任一情况:

因此 ref xV(ref x)[ref x, y] 都是延长模式,而 x&ref x&(ref x,) 则不是。

如果 let 语句中的模式是延长模式,则初始化表达式的临时作用域会延长。

基于表达式的延长

对于带有初始化表达式的 let 语句,延长表达式(extending expression) 是以下之一的表达式:

因此,&mut 0(&1, &mut 2)Some { 0: &mut 3 } 中的借用表达式都是延长表达式。&0 + &1Some(&mut 0) 中的借用则不是:后者在语法上是一个函数调用表达式。

任何延长借用表达式的操作数的临时作用域都会延长。

示例

以下是一些表达式具有延长临时作用域的示例:

#![allow(unused)]
fn main() {
fn temp() {}
trait Use { fn use_temp(&self) -> &Self { self } }
impl Use for () {}
// The temporary that stores the result of `temp()` lives in the same scope
// as x in these cases.
let x = &temp();
let x = &temp() as &dyn Send;
let x = (&*&temp(),);
let x = { [Some { 0: &temp(), }] };
let ref x = temp();
let ref x = *&temp();
x;
}

以下是一些表达式不具有延长临时作用域的示例:

#![allow(unused)]
fn main() {
fn temp() {}
trait Use { fn use_temp(&self) -> &Self { self } }
impl Use for () {}
// The temporary that stores the result of `temp()` only lives until the
// end of the let statement in these cases.

let x = Some(&temp());         // ERROR
let x = (&temp()).use_temp();  // ERROR
x;
}

不运行析构函数

手动抑制析构函数

std::mem::forget 可用于阻止变量的析构函数运行,而std::mem::ManuallyDrop 提供一个包装器来阻止变量或字段自动丢弃。

注意

即使一个变量的类型不是 'static,通过std::mem::forget 或其他方式阻止其析构函数运行也是安全的。除了本文档定义保证运行析构函数的地方之外,类型 不能 为了健全性而安全地依赖于析构函数会被运行。

不带 unwinding 的进程终止

有一些方法可以在不unwinding的情况下终止进程,在这种情况下,析构函数将不会运行。

标准库提供了std::process::exitstd::process::abort 来显式执行此操作。此外,如果恐慌处理程序(panic handler)设置为 abort,则发生恐慌将始终终止进程而不运行析构函数。

还有一个额外的情况需要注意:当恐慌到达非 unwinding 的 ABI 边界时,要么没有析构函数会运行,要么只有直到 ABI 边界的所有析构函数会运行。