丢弃检查

我们已经看到了生命周期如何为我们提供了一些相当简单的规则来确保我们永远不会读到悬空的引用。但是到目前为止,我们只是以一种包容性的方式与 outlives 关系进行了交互。也就是说,当我们谈论'a: 'b时,'a'b拥有一样长的寿命是可以的。乍一看,这似乎是一个无意义的区别。没有什么东西会和另一个东西同时被丢弃,对吗?这就是为什么我们使用了以下对let语句的解语法糖:

  1. let x;
  2. let y;

解语法糖:

  1. {
  2. let x;
  3. {
  4. let y;
  5. }
  6. }

有一些更复杂的情况不可能用作用域来解语法糖,但顺序是被定义好的——变量按其定义的相反顺序丢弃,结构体和元组的字段按其定义的顺序丢弃。在 RFC 1857 中有一些关于丢弃顺序的更多细节。

让我们来试试:

  1. let tuple = (vec![], vec![]);

左边的 Vec 先被丢弃。但这是否意味着在借用检查器的视野中,右边 Vec 的严格超过了它?这个问题的答案是 No。借用检查器可以分别跟踪元组的字段,但它仍然无法决定 Vec 元素的寿命,因为 Vec 元素是通过借用检查器不理解的纯库代码手动丢弃的。

那么,我们为什么要关心呢?我们关心是因为如果类型系统不小心,它可能会意外地产生悬空的指针。比如下面这个简单的程序:

  1. struct Inspector<'a>(&'a u8);
  2. struct World<'a> {
  3. inspector: Option<Inspector<'a>>,
  4. days: Box<u8>,
  5. }
  6. fn main() {
  7. let mut world = World {
  8. inspector: None,
  9. days: Box::new(1),
  10. };
  11. world.inspector = Some(Inspector(&world.days));
  12. }

这个程序是完全正确的,而且可以编译。事实上,days并没有严格地超过inspector的寿命,这并不重要。只要inspector还活着,days也会活着。

然而,如果我们添加一个析构器,程序就不会再编译了!

  1. struct Inspector<'a>(&'a u8);
  2. impl<'a> Drop for Inspector<'a> {
  3. fn drop(&mut self) {
  4. println!("I was only {} days from retirement!", self.0);
  5. }
  6. }
  7. struct World<'a> {
  8. inspector: Option<Inspector<'a>>,
  9. days: Box<u8>,
  10. }
  11. fn main() {
  12. let mut world = World {
  13. inspector: None,
  14. days: Box::new(1),
  15. };
  16. world.inspector = Some(Inspector(&world.days));
  17. // 如果 `days` 碰巧在这里被析构了,然后 Inspector 才被析构,就会造成`内存释放后读取`的问题!
  18. }
  1. error[E0597]: `world.days` does not live long enough
  2. --> src/main.rs:19:38
  3. |
  4. 19 | world.inspector = Some(Inspector(&world.days));
  5. | ^^^^^^^^^^^ borrowed value does not live long enough
  6. ...
  7. 22 | }
  8. | -
  9. | |
  10. | `world.days` dropped here while still borrowed
  11. | borrow might be used here, when `world` is dropped and runs the destructor for type `World<'_>`

你可以尝试改变字段的顺序,或者用一个元组来代替结构,但还是不能编译。

实现Drop可以让Inspector在被丢弃时执行一些任意的代码。这意味着它有可能观察到那些本该和它生命周期一样长的类型实际上是先被销毁的。

有趣的是,只有泛型需要担心这个问题。如果它们不是泛型的,那么它们唯一能承载的寿命就是'static,它将真正地活到永远。这就是为什么这个问题被称为 sound generic drop。健壮的泛型丢弃是由 drop checker 强制执行的。截止到本文写作时,关于丢弃检查器(也被称为dropck)如何验证类型的一些更细微的细节还完全是未知数。然而,“大规则”是我们这一节所关注的微妙之处:

对于一个泛型类型来说,要健壮地实现 drop,其泛型参数必须严格超过它的寿命。

遵守这一规则(通常)是满足借用检查器的必要条件;遵守这一规则是充分的,但不是健壮的必要条件。也就是说,如果你的类型遵守了这个规则,那么它的 drop 肯定是健壮的。

不一定要满足上述规则的原因是,有些 Drop 实现不会访问借来的数据,即使他们的类型给了他们这种访问的能力,或者因为我们知道具体的 Drop 顺序,即使借用检查器不知道。

例如,上述Inspector例子的这个变体永远不会访问借来的数据:

  1. struct Inspector<'a>(&'a u8, &'static str);
  2. impl<'a> Drop for Inspector<'a> {
  3. fn drop(&mut self) {
  4. println!("Inspector(_, {}) knows when *not* to inspect.", self.1);
  5. }
  6. }
  7. struct World<'a> {
  8. inspector: Option<Inspector<'a>>,
  9. days: Box<u8>,
  10. }
  11. fn main() {
  12. let mut world = World {
  13. inspector: None,
  14. days: Box::new(1),
  15. };
  16. world.inspector = Some(Inspector(&world.days, "gadget"));
  17. // 假设 `days` 刚好在这里析构了,
  18. // 并且假设析构函数可以确保:该函数确保不会访问对 `days` 的引用
  19. }

同样地,下面这个变体也不会访问借来的数据:

  1. struct Inspector<T>(T, &'static str);
  2. impl<T> Drop for Inspector<T> {
  3. fn drop(&mut self) {
  4. println!("Inspector(_, {}) knows when *not* to inspect.", self.1);
  5. }
  6. }
  7. struct World<T> {
  8. inspector: Option<Inspector<T>>,
  9. days: Box<u8>,
  10. }
  11. fn main() {
  12. let mut world = World {
  13. inspector: None,
  14. days: Box::new(1),
  15. };
  16. world.inspector = Some(Inspector(&world.days, "gadget"));
  17. // 假设 `days` 刚好在这里析构了,
  18. // 并且假设析构函数可以确保:该函数确保不会访问对 `days` 的引用
  19. }

然而,上述两种变体在分析fn main时都被借用检查器拒绝了,说days的生命周期不够长。

原因是对main的借用检查分析并不了解每个InspectorDrop实现的内部情况。就借用检查器在分析main时知道的情况来看,检查器的析构器主体可能会访问这些借用的数据。

因此,丢弃检查器强迫一个值中的所有借用数据的生命周期严格地超过该值的生命周期。

一种逃逸方法

丢弃检查的精确规则在未来可能会减少限制。

目前的分析是故意保守和琐碎的;它强制一个值中的所有借来的数据的生命周期超过该值的生命周期,这当然是合理的。

未来版本的语言可能会使分析更加精确,以减少健壮的代码被拒绝为不安全的情况。这将有助于解决诸如上述两个Inspector知道在销毁时不访问借来的数据的情况。

但与此同时,有一个不稳定的属性,可以用来断言(不安全的)通用类型的析构器 保证 不访问任何过期数据,即使它的类型赋予它这样的能力。

这个属性被称为may_dangle,是在RFC1327中引入的。要在上面的Inspector上用上它,我们可以这么写:

  1. #![feature(dropck_eyepatch)]
  2. struct Inspector<'a>(&'a u8, &'static str);
  3. unsafe impl<#[may_dangle] 'a> Drop for Inspector<'a> {
  4. fn drop(&mut self) {
  5. println!("Inspector(_, {}) knows when *not* to inspect.", self.1);
  6. }
  7. }
  8. struct World<'a> {
  9. days: Box<u8>,
  10. inspector: Option<Inspector<'a>>,
  11. }
  12. fn main() {
  13. let mut world = World {
  14. inspector: None,
  15. days: Box::new(1),
  16. };
  17. world.inspector = Some(Inspector(&world.days, "gatget"));
  18. }

使用这个属性需要将Drop标记为unsafe,因为编译器没有检查隐含的断言,即没有访问潜在的过期数据(例如上面的self.0)。

该属性可以应用于任何数量的生命周期和类型参数。在下面的例子中,我们断言我们没有访问寿命为'b的引用后面的数据,并且T的唯一用途是 move 或 drop,但是从'aU中省略了该属性,因为我们确实访问具有该生命周期和该类型的数据。

  1. #![feature(dropck_eyepatch)]
  2. use std::fmt::Display;
  3. struct Inspector<'a, 'b, T, U: Display>(&'a u8, &'b u8, T, U);
  4. unsafe impl<'a, #[may_dangle] 'b, #[may_dangle] T, U: Display> Drop for Inspector<'a, 'b, T, U> {
  5. fn drop(&mut self) {
  6. println!("Inspector({}, _, _, {})", self.0, self.3);
  7. }
  8. }

有时很明显,不可能发生这样的访问,比如上面的情况。然而,当处理一个通用类型的参数时,这种访问可能会间接地发生,这种间接访问的例子是:

  • 调用一个回调
  • 通过 trait 方法调用

(未来对语言的修改,如 impl 的特化,可能会增加这种间接访问的其他途径。)

下面是一个回调的例子:

  1. struct Inspector<T>(T, &'static str, Box<for <'r> fn(&'r T) -> String>);
  2. impl<T> Drop for Inspector<T> {
  3. fn drop(&mut self) {
  4. // 如果 `T` 是 `&'a _` 这种类型,那么 self.2 有可能访问了被引用的变量
  5. println!("Inspector({}, {}) unwittingly inspects expired data.",
  6. (self.2)(&self.0), self.1);
  7. }
  8. }

下面是一个通过 trait 方法调用的例子:

  1. use std::fmt;
  2. struct Inspector<T: fmt::Display>(T, &'static str);
  3. impl<T: fmt::Display> Drop for Inspector<T> {
  4. fn drop(&mut self) {
  5. // 这里可能隐藏了一个对于 `<T as Display>::fmt` 的调用,
  6. // 如果 `T` 是 `&'a _` 这种类型,就可能访问了借用的变量
  7. println!("Inspector({}, {}) unwittingly inspects expired data.",
  8. self.0, self.1);
  9. }
  10. }

当然,所有这些访问都可以进一步隐藏在由析构器调用的一些其他方法中,而不是直接写在析构器中。

在上述所有在析构器中访问&'a u8的情况下,添加#[may_dangle]属性使得该类型容易被误用,而借用检查器不会发现,从而导致问题。所以最好不要添加这个属性。

关于丢弃顺序的附带说明

虽然结构内部字段的删除顺序是被定义的,但对它的依赖是脆弱而微妙的。当顺序很重要时,最好使用ManuallyDrop包装器。

这就是关于丢弃检查器的全部内容吗?

事实证明,在编写不安全的代码时,我们通常根本不需要担心为丢弃检查器做正确的事情。然而,有一种特殊情况是需要担心的,我们将在下一节看一下。