丢弃标志
上一节的例子给 Rust 引入了一个有趣的问题。我们已经看到,完全可以安全地有条件地初始化、反初始化和重新初始化内存位置。对于 Copy 类型来说,这没什么特别的,因为它们只是一堆随机的比特。但是,带有析构函数的类型就不同了:当一个变量被赋值或超出作用域时,Rust 需要知道是否调用析构函数。它如何通过条件初始化来实现这一点呢?
请注意,并非所有赋值都需要担心这个问题。特别是,通过解引用赋值会无条件地丢弃,而在 let
中赋值不会无条件地丢弃。
#![allow(unused)] fn main() { let mut x = Box::new(0); // let makes a fresh variable, so never need to drop let y = &mut x; *y = Box::new(1); // Deref assumes the referent is initialized, so always drops }
只有在覆盖先前初始化的变量或其子字段时,这才是问题。
事实证明,Rust 实际上在运行时跟踪一个类型是否应该被丢弃。当一个变量被初始化和未初始化时,该变量的丢弃标志会被切换。当一个变量可能需要被丢弃时,会评估此标志以确定是否应该被丢弃。
当然,通常情况下,一个值的初始化状态在程序的每个点都可以静态地知道。如果是这样,那么编译器理论上可以生成更高效的代码!例如,直线代码具有这种静态丢弃语义。
#![allow(unused)] fn main() { let mut x = Box::new(0); // x was uninit; just overwrite. let mut y = x; // y was uninit; just overwrite and make x uninit. x = Box::new(0); // x was uninit; just overwrite. y = x; // y was init; Drop y, overwrite it, and make x uninit! // y goes out of scope; y was init; Drop y! // x goes out of scope; x was uninit; do nothing. }
类似地,所有分支在初始化方面具有相同行为的分支代码也具有静态丢弃语义。
#![allow(unused)] fn main() { let condition = true; let mut x = Box::new(0); // x was uninit; just overwrite. if condition { drop(x) // x gets moved out; make x uninit. } else { println!("{}", x); drop(x) // x gets moved out; make x uninit. } x = Box::new(0); // x was uninit; just overwrite. // x goes out of scope; x was init; Drop x! }
然而,像这样的代码需要运行时信息才能正确地 Drop。
#![allow(unused)] fn main() { let condition = true; let x; if condition { x = Box::new(0); // x was uninit; just overwrite. println!("{}", x); } // x goes out of scope; x might be uninit; // check the flag! }
当然,在这种情况下,检索静态丢弃语义是微不足道的。
#![allow(unused)] fn main() { let condition = true; if condition { let x = Box::new(0); println!("{}", x); } }
丢弃标志在堆栈上跟踪。在旧的 Rust 版本中,丢弃标志被存放在实现了 Drop
的类型的隐藏字段中。