丢弃标志

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

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

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

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

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

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

#![allow(unused)]
fn main() {
let mut x = Box::new(0); // x 未初始化;仅覆盖值
let mut y = x;           // y 未初始化;仅覆盖值,并设置 x 为未初始化
x = Box::new(0);         // x 未初始化;仅覆盖值
y = x;                   // y 已初始化;销毁 y,覆盖它的值,设置 x 为未初始化
                         // y 离开作用域;y 已初始化;销毁 y
                         // x 离开作用域;x 未初始化;什么都不用做
}

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

#![allow(unused)]
fn main() {
let condition = true;
let mut x = Box::new(0); // x 未初始化;仅覆盖值
if condition {
    drop(x);             // x 失去值;设置 x 为未初始化
} else {
    println!("{}", x);
    drop(x);             // x 失去值;设置 x 为未初始化
}
x = Box::new(0);         // x 未初始化;仅覆盖值
                         // x 离开作用域;x 已初始化;销毁 x
}

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

#![allow(unused)]
fn main() {
let condition = true;
let x;
if condition {
    x = Box::new(0);        // x 未初始化;仅覆盖值
    println!("{}", x);
}
                            // x 离开了作用域,可能未初始化
                            // 检查 drop 标志位!
}

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

#![allow(unused)]
fn main() {
let condition = true;
if condition {
    let x = Box::new(0);
    println!("{}", x);
}
}

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