Drop 检查

我们已经了解了生命周期如何为我们提供一些相当简单的规则,以确保我们永远不会读取悬垂引用。然而,到目前为止,我们只以包含的方式与外生存关系进行交互。也就是说,当我们谈论'a: 'b时,'a 恰好与 'b 活得一样长是可以接受的。乍一看,这似乎是一个毫无意义的区别。没有任何东西会与其他东西同时被释放,对吗?这就是为什么我们使用以下 let 语句的去糖化

let x;
let y;

去糖化为

{
    let x;
    {
        let y;
    }
}

还有一些更复杂的情况无法使用作用域进行去糖化,但顺序仍然是定义的 ‒ 变量以其定义相反的顺序释放,结构体和元组的字段以其定义的顺序释放。关于释放顺序的更多详细信息请参考 RFC 1857

让我们开始吧

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

左边的向量首先被释放。但这是否意味着在借用检查器的眼中,右边的向量严格地比它活得更久?这个问题的答案是。借用检查器可以单独跟踪元组的字段,但它仍然无法确定在向量元素的情况下,哪个比哪个活得更久,因为向量元素是通过借用检查器不理解的纯库代码手动释放的。

那么我们为什么要关心呢?我们关心是因为如果类型系统不小心,它可能会意外地创建悬垂指针。考虑以下简单的程序

struct Inspector<'a>(&'a u8);

struct World<'a> {
    inspector: Option<Inspector<'a>>,
    days: Box<u8>,
}

fn main() {
    let mut world = World {
        inspector: None,
        days: Box::new(1),
    };
    world.inspector = Some(Inspector(&world.days));
}

这个程序完全健全,并且今天可以编译。days 没有严格地比 inspector 活得更久的事实并不重要。只要 inspector 还活着,days 也还活着。

但是,如果我们添加一个析构函数,程序将不再编译!

struct Inspector<'a>(&'a u8);

impl<'a> Drop for Inspector<'a> {
    fn drop(&mut self) {
        println!("I was only {} days from retirement!", self.0);
    }
}

struct World<'a> {
    inspector: Option<Inspector<'a>>,
    days: Box<u8>,
}

fn main() {
    let mut world = World {
        inspector: None,
        days: Box::new(1),
    };
    world.inspector = Some(Inspector(&world.days));
    // Let's say `days` happens to get dropped first.
    // Then when Inspector is dropped, it will try to read free'd memory!
}
error[E0597]: `world.days` does not live long enough
  --> src/main.rs:19:38
   |
19 |     world.inspector = Some(Inspector(&world.days));
   |                                      ^^^^^^^^^^^ borrowed value does not live long enough
...
22 | }
   | -
   | |
   | `world.days` dropped here while still borrowed
   | borrow might be used here, when `world` is dropped and runs the destructor for type `World<'_>`

您可以尝试更改字段的顺序或使用元组而不是结构体,它仍然不会编译。

实现 Drop 可以让 Inspector 在其死亡期间执行一些任意代码。这意味着它可能会观察到那些应该和它活得一样长的类型实际上是先被销毁的。

有趣的是,只有泛型类型才需要担心这个问题。如果它们不是泛型的,那么它们可以容纳的唯一生命周期是 'static,它将真正地永远存在。这就是为什么这个问题被称为健全的泛型释放。健全的泛型释放由释放检查器强制执行。在撰写本文时,释放检查器(也称为 dropck)如何验证类型的某些更精细的细节完全不确定。然而,我们整个章节都关注的重点是大规则

为了让泛型类型可以健全地实现释放,其泛型参数必须严格地比它活得更久。

遵守此规则(通常)是满足借用检查器的必要条件;遵守此规则是足够但不必要的前提,以保持健全。也就是说,如果您的类型遵守此规则,那么它的释放肯定是健全的。

之所以不总是必须满足上述规则,是因为某些 Drop 实现不会访问借用的数据,即使它们的类型赋予了它们这种访问能力,或者因为我们知道特定的释放顺序,即使借用检查器不知道,借用的数据仍然没问题。

例如,上面 Inspector 示例的这个变体永远不会访问借用的数据

struct Inspector<'a>(&'a u8, &'static str);

impl<'a> Drop for Inspector<'a> {
    fn drop(&mut self) {
        println!("Inspector(_, {}) knows when *not* to inspect.", self.1);
    }
}

struct World<'a> {
    inspector: Option<Inspector<'a>>,
    days: Box<u8>,
}

fn main() {
    let mut world = World {
        inspector: None,
        days: Box::new(1),
    };
    world.inspector = Some(Inspector(&world.days, "gadget"));
    // Let's say `days` happens to get dropped first.
    // Even when Inspector is dropped, its destructor will not access the
    // borrowed `days`.
}

同样,这个变体也永远不会访问借用的数据

struct Inspector<T>(T, &'static str);

impl<T> Drop for Inspector<T> {
    fn drop(&mut self) {
        println!("Inspector(_, {}) knows when *not* to inspect.", self.1);
    }
}

struct World<T> {
    inspector: Option<Inspector<T>>,
    days: Box<u8>,
}

fn main() {
    let mut world = World {
        inspector: None,
        days: Box::new(1),
    };
    world.inspector = Some(Inspector(&world.days, "gadget"));
    // Let's say `days` happens to get dropped first.
    // Even when Inspector is dropped, its destructor will not access the
    // borrowed `days`.
}

然而,在分析 fn main 期间,以上两个变体都被借用检查器拒绝,指出 days 活得不够长。

原因是 main 的借用检查分析不了解每个 InspectorDrop 实现的内部结构。就借用检查器在分析 main 时所知,inspector 的析构函数的主体可能会访问该借用的数据。

因此,释放检查器强制值中的所有借用数据都严格地比该值活得更久。

一个逃生舱口

未来,控制释放检查的精确规则可能会减少限制。

当前的分析是故意保守和简单的;它强制值中的所有借用数据都比该值活得更久,这肯定是健全的。

未来版本的语言可能会使分析更精确,以减少将健全代码作为不安全代码拒绝的情况。这将有助于解决诸如上述两个 Inspector 的情况,它们知道在销毁期间不要检查。

同时,有一个不稳定的属性可以用来断言(不安全地)泛型类型的析构函数保证不会访问任何过期的数据,即使其类型赋予了它这样做的能力。

该属性称为 may_dangle,是在 RFC 1327 中引入的。为了在上面的 Inspector 上部署它,我们将写成

#![feature(dropck_eyepatch)]

struct Inspector<'a>(&'a u8, &'static str);

unsafe impl<#[may_dangle] 'a> Drop for Inspector<'a> {
    fn drop(&mut self) {
        println!("Inspector(_, {}) knows when *not* to inspect.", self.1);
    }
}

struct World<'a> {
    days: Box<u8>,
    inspector: Option<Inspector<'a>>,
}

fn main() {
    let mut world = World {
        inspector: None,
        days: Box::new(1),
    };
    world.inspector = Some(Inspector(&world.days, "gadget"));
}

使用此属性需要将 Drop impl 标记为 unsafe,因为编译器不检查隐式断言,即没有访问任何可能过期的数据(例如,上面的 self.0)。

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

#![allow(unused)]
#![feature(dropck_eyepatch)]
fn main() {
use std::fmt::Display;

struct Inspector<'a, 'b, T, U: Display>(&'a u8, &'b u8, T, U);

unsafe impl<'a, #[may_dangle] 'b, #[may_dangle] T, U: Display> Drop for Inspector<'a, 'b, T, U> {
    fn drop(&mut self) {
        println!("Inspector({}, _, _, {})", self.0, self.3);
    }
}
}

有时很明显,不会发生此类访问,就像上面的情况一样。但是,在处理泛型类型参数时,可能会间接发生此类访问。此类间接访问的示例包括

  • 调用回调,
  • 通过 trait 方法调用。

(未来对语言的更改,例如 impl 特化,可能会为此类间接访问增加其他途径。)

这是一个调用回调的示例

#![allow(unused)]
fn main() {
struct Inspector<T>(T, &'static str, Box<for <'r> fn(&'r T) -> String>);

impl<T> Drop for Inspector<T> {
    fn drop(&mut self) {
        // The `self.2` call could access a borrow e.g. if `T` is `&'a _`.
        println!("Inspector({}, {}) unwittingly inspects expired data.",
                 (self.2)(&self.0), self.1);
    }
}
}

这是一个 trait 方法调用的示例

#![allow(unused)]
fn main() {
use std::fmt;

struct Inspector<T: fmt::Display>(T, &'static str);

impl<T: fmt::Display> Drop for Inspector<T> {
    fn drop(&mut self) {
        // There is a hidden call to `<T as Display>::fmt` below, which
        // could access a borrow e.g. if `T` is `&'a _`
        println!("Inspector({}, {}) unwittingly inspects expired data.",
                 self.0, self.1);
    }
}
}

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

在以上所有在析构函数中访问 &'a u8 的情况下,添加 #[may_dangle] 属性会使该类型容易受到借用检查器不会捕获的滥用,从而导致混乱。最好避免添加该属性。

虽然定义了结构体内部字段的释放顺序,但依赖它是脆弱且微妙的。当顺序很重要时,最好使用 ManuallyDrop 包装器。

这就是关于释放检查器的全部内容吗?

事实证明,在编写不安全的代码时,我们通常根本不需要担心为释放检查器做正确的事情。但是,有一个特殊的情况需要您担心,我们将在下一节中讨论。