放置检查

我们已经了解了生命周期如何为我们提供一些相当简单的规则,以确保我们永远不会读取悬空引用。然而,到目前为止,我们只以一种包含的方式与*outlives*关系进行交互。也就是说,当我们谈论 '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 在销毁期间知道不进行检查的案例。

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

该属性称为 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 包装器。

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

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