丢弃标志

上一节的例子为 Rust 引入了一个有趣的问题。我们已经看到,可以完全安全地对内存位置进行有条件的初始化、非初始化和重新初始化。对于实现了Copy的类型来说,这并不特别值得注意,因为它们只是一堆随机的比特。然而,带有析构器的类型是一个不同的故事。Rust 需要知道每当一个变量被赋值,或者一个变量超出范围时,是否要调用一个析构器。它怎么能用条件初始化来做到这一点呢?

请注意,这不是所有赋值都需要担心的问题。特别是,通过解引用的赋值会无条件地被丢弃,而相对的,在let中的赋值无论如何都不会被丢弃:

  1. let mut x = Box::new(0); // let 创建了一个全新的变量,所以一定(也没有必要)调用 drop
  2. let y = &mut x;
  3. *y = Box::new(1); // 解引用假设原先的变量已经初始化了,因此一定会 drop

仅当覆盖先前初始化的变量或其子字段之一时,这才是个问题。

这种情况下,Rust 实际上是在运行时跟踪一个类型是否应该被丢弃。当一个变量被初始化和未初始化时,该变量的drop flag被切换。当一个变量可能需要被丢弃时,这个标志会被读取,以确定它是否应该被丢弃。

当然,通常的情况是,一个值的初始化状态在程序的每一个点上都是静态已知的。如果是这种情况,那么编译器理论上可以生成更有效的代码。例如,直线型代码就有这样的静态丢弃语义(static drop semantics)

  1. let mut x = Box::new(0); // x 未初始化;仅覆盖值
  2. let mut y = x; // y 未初始化;仅覆盖值,并设置 x 为未初始化
  3. x = Box::new(0); // x 未初始化;仅覆盖值
  4. y = x; // y 已初始化;销毁 y,覆盖它的值,设置 x 为未初始化
  5. // y 离开作用域;y 已初始化;销毁 y
  6. // x 离开作用域;x 未初始化;什么都不用做

类似地,所有分支都在初始化方面具有相同行为的代码具有静态丢弃语义:

  1. # let condition = true;
  2. let mut x = Box::new(0); // x 未初始化;仅覆盖值
  3. if condition {
  4. drop(x); // x 失去值;设置 x 为未初始化
  5. } else {
  6. println!("{}", x);
  7. drop(x); // x 失去值;设置 x 为未初始化
  8. }
  9. x = Box::new(0); // x 未初始化;仅覆盖值
  10. // x 离开作用域;x 已初始化;销毁 x

然而像这样的代码需要运行时的信息来正确地 Drop:

  1. # let condition = true;
  2. let x;
  3. if condition {
  4. x = Box::new(0); // x 未初始化;仅覆盖值
  5. println!("{}", x);
  6. }
  7. // x 离开了作用域,可能未初始化
  8. // 检查 drop 标志位!

当然,在这种情况下,获得静态丢弃语义是很简单的:

  1. # let condition = true;
  2. if condition {
  3. let x = Box::new(0);
  4. println!("{}", x);
  5. }

丢弃标志在栈中被跟踪。 在旧的 Rust 版本中,丢弃标志曾经是隐藏在实现Drop的类型中。