析构函数

当一个 已初始化过变量临时值 超出 作用域 时,会执行其 析构函数 会,或者说会被 销毁赋值 时会运行其已初始化的左操作数的析构函数。 如果变量只有部分已初始化,那么只销毁已初始化字段。

类型 T 的析构函数包括:

  1. 如果 T: Drop ,则调用 <T as std::ops::Drop>::drop
  2. 递归运行其所有字段的析构函数。
    • 结构体 的字段按声明顺序被销毁。
    • 枚举类型 的激活变体字段按声明顺序被销毁。
    • 元组 的字段按顺序被销毁。
    • 数组 或拥有的 切片 的元素从第一个元素到最后一个元素被销毁。
    • 闭包 通过移动捕获的变量按一个未指定的顺序被销毁。
    • 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"); 
// Tuple 的元素按顺序销毁
let tuple = (PrintOnDrop("Tuple first"), PrintOnDrop("Tuple second")); 

let moved;
// 在赋值时不会运行析构函数
moved = PrintOnDrop("Drops when moved"); 
// 现在被销毁,但后来处于未初始化状态
moved;

// 未初始化不会被销毁
let uninitialized: PrintOnDrop;

// 在部分移动后,只有剩余的字段会被销毁
let mut partial_move = (PrintOnDrop("first"), PrintOnDrop("forgotten"));
// 进行部分移动,只剩下 `partial_move.0` 被初始化
core::mem::forget(partial_move.1);
// 当 `partial_move` 的作用域结束时,只有第一个字段被销毁。
}

析构作用域

每个变量或临时变量都与一个 析构作用域 相关联。 当控制流离开析构作用域时,与该作用域相关联的所有变量按照声明 (对于变量) 或创建 (对于临时变量) 的相反顺序进行销毁。 在使用 match 表达式替换 forif letwhile let 表达式之后,确定析构作用域。 重载的操作符与内置的操作符没有区别,不考虑绑定模式。 对于函数或闭包,有以下析构作用域:

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

析构作用域按如下嵌套。当一次离开多个作用域时,例如从函数返回时,变量从内向外进行销毁。

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

函数参数的作用域

所有函数参数都在整个函数体的作用域内,参数被最后丢弃。 函数的实参在该参数模式中引入后,其绑定之后被丢弃。

#![allow(unused)]
fn main() {
struct PrintOnDrop(&'static str);
impl Drop for PrintOnDrop {
    fn drop(&mut self) {
        println!("drop({})", self.0);
    }
}
// 按顺序丢弃 `y`、第二个参数、`x`,最后第一个参数
fn patterns_in_parameters(
    (x, _): (PrintOnDrop, PrintOnDrop),
    (_, y): (PrintOnDrop, PrintOnDrop),
) {}

// 丢弃顺序为 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");
}

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

临时变量的作用域

一个表达式的 临时作用域 是在 占位上下文 中使用该表达式时用于保存该表达式结果的临时变量的作用域,除非该表达式被 提升

除了生命周期扩展之外,表达式的临时作用域是包含表达式的最小作用域,可以是以下之一:

  • 整个函数体。
  • 一个语句。
  • ifwhileloop 表达式的主体。
  • if 表达式的 else 块。
  • ifwhile 表达式的条件表达式,或 match 守卫。
  • match 分支的主体表达式。
  • 惰性布尔表达式的第二个操作数。

:

在函数体的最终表达式中创建的临时变量将在函数体绑定的任何命名变量之后释放,因为没有更小的封闭临时作用域。

match 表达式的 被匹配项 不是临时作用域,因此可以在 match 表达式之后丢弃被匹配项中的临时变量。 例如,在 match 1 { ref mut z => z }; 中的 1 的临时变量将一直存在到语句的结尾。

一些例子:

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

// 一旦条件被评估,将被丢弃
if PrintOnDrop("if条件").0 == "if条件" {
    // 在块的末尾被丢弃
    PrintOnDrop("if块体").0
} else {
    unreachable!()
};

// 在语句的末尾被丢弃
(PrintOnDrop("第一个操作数").0 == ""
// 在 ) 处被丢弃
|| PrintOnDrop("第二个操作数").0 == "")
// 在表达式的末尾被丢弃
|| PrintOnDrop("第三个操作数").0 == "";

// 在函数的末尾被丢弃,在局部变量之后。
// 将其更改为包含返回表达式的语句将使临时变量在局部变量之前被释放。
// 将其绑定到变量并返回该变量也会使临时变量首先被释放。
match PrintOnDrop("匹配的值,用于最终表达式") {
    // 一旦条件被评估,将被丢弃
    _ if PrintOnDrop("守卫条件").0 == "" => (),
    _ => (),
}
}

操作数

在表达式中,为了保存操作数的结果,也会创建临时变量,而计算其他操作数。 这些临时变量与该操作数的表达式作用域相关联。 由于临时变量在计算表达式后被移动,除非表达式的操作数之一中断表达式、返回或抛出异常,否则将其丢弃不会产生副作用。

#![allow(unused)]
fn main() {
struct PrintOnDrop(&'static str);
impl Drop for PrintOnDrop {
    fn drop(&mut self) {
        println!("drop({})", self.0);
    }
}
loop {
    // 元组表达式没有完成评估,因此操作数按相反的顺序丢弃
    (
        PrintOnDrop("外部元组第一个"),
        PrintOnDrop("外部元组第二个"),
        (
            PrintOnDrop("内部元组第一个"),
            PrintOnDrop("内部元组第二个"),
            break, // 循环中断,后面的表达式不会执行
        ),
        PrintOnDrop("永远不会创建"),
    );
}
}

常量晋升

将值表达式晋升为 'static 发生在表达式写成常量并借用时,其借用可以在不改变运行时行为的情况下,在表达式原始写法的位置进行解引用。 也就是说,晋升的表达式可以在编译时计算,所得到的值不包含 内部可变性析构函数 (这些属性在可能的情况下基于值来确定,例如, &None 始终具有类型 &'static Option<_> ,因为它不包含任何被禁止的内容)。

临时值生命周期延长

注意: 临时值生命周期延长的确切规则可能会发生变化。本文描述的是当前行为。

let 语句中的表达式的临时值作用域有时会被 延长 到包含 let 语句的块的作用域。 这是根据某些语法规则判断,当通常的临时值作用域太小时进行的。例如:

#![allow(unused)]
fn main() {
let x = &mut 0;
// 通常此时一个临时变量已经被丢弃了,但是 `0` 的临时变量会一直存活到该块的结尾。
println!("{}", x);
}

如果一个 借用表达式解引用表达式字段表达式元组索引表达式 具有扩展临时作用域,则其操作数也具有扩展临时作用域。 如果一个 索引表达式 具有扩展临时作用域,则索引表达式也具有扩展临时作用域。

基于模式的扩展

一个 扩展模式 可以是:

  • 一个通过引用或可变引用绑定的 标识符模式
  • 一个 结构体模式元组模式元组结构体模式切片模式 ,其中至少一个直接子模式是扩展模式。 因此, ref xV(ref x)[ref x, y] 都是扩展模式,但 x&ref x&(ref x,) 不是。如果 let 语句中的模式是扩展模式,则初始化表达式的临时作用域会被扩展。

基于表达式的扩展

对于带有初始化器的 let 语句,一个 扩展表达式 是以下表达式之一:

因此, &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 () {}
// 存储 `temp()` 结果的临时变量在以下这些情况下与 x 具有相同的作用域。
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 () {}
// 存储`temp()`结果的临时变量在以下这些情况下只能在 let 语句结束之前使用。

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

不运行析构函数

std::mem::forget 可以用来防止变量的析构函数被执行,而 std::mem::ManuallyDrop 提供了一个包装器来防止变量或字段被自动释放。

注意:通过 std::mem::forget 或其他方式防止析构函数被执行即使具有不是 'static 类型的类型也是安全的。 除了在本文档中定义的保证运行析构函数的地方之外,类型不能依赖析构函数被运行来确保安全性。