PhantomData

当使用不安全代码时,我们经常会遇到类型或生命周期在逻辑上与结构体相关联,但实际上不是字段一部分的情况。这种情况最常见于生命周期。例如,&'a [T]Iter (近似)定义如下:

#![allow(unused)]
fn main() {
struct Iter<'a, T: 'a> {
    ptr: *const T,
    end: *const T,
}
}

然而,因为 'a 在结构体的主体中没有被使用,它是无界的。 由于历史上由此引起的问题,无界的生命周期和类型在结构体定义中是禁止的。因此,我们必须以某种方式在主体中引用这些类型。正确地这样做对于拥有正确的变性和 drop 检查是必要的。

我们使用 PhantomData 来做到这一点,它是一个特殊的标记类型。 PhantomData 不占用空间,但为了静态分析的目的,它模拟给定类型的字段。这被认为比显式告诉类型系统你想要的变性类型更不容易出错,同时也提供了其他有用的东西,例如自动 traits 和 drop 检查所需的信息。

Iter 逻辑上包含了一堆 &'a T,所以这正是我们告诉 PhantomData 要模拟的内容:

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

struct Iter<'a, T: 'a> {
    ptr: *const T,
    end: *const T,
    _marker: marker::PhantomData<&'a T>,
}
}

就是这样。生命周期将被绑定,并且你的迭代器将对 'aT 是协变的。一切都正常运行。

泛型参数和drop检查

过去,还有另一件事需要考虑。

这篇文档过去常常说:

另一个重要的例子是 Vec,它的(近似)定义如下:

#![allow(unused)]
fn main() {
struct Vec<T> {
    data: *const T, // *const for variance!
    len: usize,
    cap: usize,
}
}

与之前的例子不同,它看起来一切都如我们所愿。Vec 的每个泛型参数都至少出现在一个字段中。一切就绪!

不。

drop 检查器会慷慨地确定 Vec<T> 不拥有任何 T 类型的值。这将反过来使其得出结论,它不需要担心 Vec 在其析构函数中 drop 任何 T,从而确定 drop 检查的健全性。这将反过来允许人们使用 Vec 的析构函数来创建不健全的代码。

为了告诉 drop 检查器我们确实拥有 T 类型的值,因此在我们 drop 时可能会 drop 一些 T,我们必须添加一个额外的 PhantomData 来明确说明:

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

struct Vec<T> {
    data: *const T, // *const for variance!
    len: usize,
    cap: usize,
    _owns_T: marker::PhantomData<T>,
}
}

但是自从 RFC 1238 以来,这不再是正确的,也没有必要

如果你要写

#![allow(unused)]
fn main() {
struct Vec<T> {
    data: *const T, // `*const` for variance!
    len: usize,
    cap: usize,
}

#[cfg(any())]
impl<T> Drop for Vec<T> { /* … */ }
}

那么 impl<T> Drop for Vec<T> 的存在使得 Rust 会认为 Vec<T> 拥有 T 类型的值(更准确地说:可能会在其 Drop 实现中使用 T 类型的值),因此 Rust 不会允许它们在 Vec<T> 被 drop 时悬空

当类型已经有 Drop impl 时,添加额外的 _owns_T: PhantomData<T> 字段是多余的,并且在 drop 检查方面没有任何作用(它仍然会影响变性和自动 traits)。

  • (高级边缘情况:如果包含 PhantomData 的类型根本没有 Drop impl,但仍然有 drop 胶水(通过拥有另一个带有 drop 胶水的字段),那么这里提到的 drop 检查/#[may_dangle] 考虑也适用:当包含的类型超出范围时,PhantomData<T> 字段将要求 T 可 drop)。

但这种情况有时会导致代码过于严格。这就是为什么标准库使用一个不稳定的和 unsafe 的属性来选择回到旧的“未检查”的 drop 检查行为,而这篇文档警告过:#[may_dangle] 属性。

一个例外:标准库及其不稳定的 #[may_dangle] 的特殊情况

如果你只是编写自己的库代码,可以跳过这一部分;但是如果你对标准库如何使用实际的 Vec 定义感到好奇,你会注意到它仍然需要使用 _owns_T: PhantomData<T> 字段来保证健全性。

点击这里查看原因

考虑以下示例

fn main() {
    let mut v: Vec<&str> = Vec::new();
    let s: String = "Short-lived".into();
    v.push(&s);
    drop(s);
} // <- `v` is dropped here

对于经典的 impl<T> Drop for Vec<T> { 定义,上面的代码 被拒绝了

实际上,在这种情况下,我们有一个 Vec</* T = */ &'s str> 向量,其中包含对 str 字符串的 's 生命周期引用,但在 let s: String 的情况下,它在 Vec 之前被 drop,因此当 Vec 被 drop 并且使用 impl<'s> Drop for Vec<&'s str> { 时,'s 已过期

这意味着如果使用这样的 Drop,它将处理一个过期悬空的生命周期 's。但这与 Rust 原则相反,在 Rust 原则中,默认情况下,函数签名中涉及的所有 Rust 引用都是非悬空的,并且可以安全地解引用。

因此,Rust 必须保守地拒绝这个代码片段。

然而,在真正的 Vec 的情况下,Drop impl 并不关心 &'s str因为它本身没有 drop 胶水:它只想释放后备缓冲区。

换句话说,如果上面的代码片段能够通过特殊处理 Vec,或者依赖 Vec 的某些特殊属性来接受就好了:Vec 可以尝试承诺在被 drop 时不使用它所持有的 &'s str

这是可以用 #[may_dangle] 表示的 unsafe 承诺类型:

unsafe impl<#[may_dangle] 's> Drop for Vec<&'s str> { /* … */ }

或者,更一般地说:

unsafe impl<#[may_dangle] T> Drop for Vec<T> { /* … */ }

是一种 unsafe 的方式,可以选择退出 Rust 的 drop 检查器关于被 drop 实例的类型参数不允许悬空的保守假设。

当这样做时,比如在标准库中,我们需要小心 T 本身有 drop 胶水的情况。在这种情况下,想象一下用一个 struct PrintOnDrop<'s> /* = */ (&'s str); 替换 &'s str,它会有一个 Drop impl,其中内部的 &'s str 将被解引用并打印到屏幕上。

实际上,Drop for Vec<T> { 在释放后备缓冲区之前,必须传递地 drop 每个具有 drop 胶水的 T 项;在 PrintOnDrop<'s> 的情况下,这意味着 Drop for Vec<PrintOnDrop<'s>> 必须在释放后备缓冲区之前传递地 drop PrintOnDrop<'s> 元素。

所以当我们说 's #[may_dangle] 时,这是一个过于宽松的声明。我们宁愿说:“'s 可以悬空,只要它不涉及任何传递性的 drop 胶水”。或者,更一般地说,“T 可以悬空,只要它不涉及任何传递性的 drop 胶水”。只要我们拥有 T,这种“例外的例外”就是一种普遍存在的情况。这就是为什么 Rust 的 #[may_dangle] 足够聪明,能够知道这种选择退出,因此当泛型参数以拥有的方式被结构体的字段持有,它将被禁用。

这就是为什么标准库最终会得到:

#![allow(unused)]
fn main() {
#[cfg(any())]
// we pinky-swear not to use `T` when dropping a `Vec`…
unsafe impl<#[may_dangle] T> Drop for Vec<T> {
    fn drop(&mut self) {
        unsafe {
            if mem::needs_drop::<T>() {
                /* … except here, that is, … */
                ptr::drop_in_place::<[T]>(/* … */);
            }
            // …
            dealloc(/* … */)
            // …
        }
    }
}

struct Vec<T> {
    // … except for the fact that a `Vec` owns `T` items and
    // may thus be dropping `T` items on drop!
    _owns_T: core::marker::PhantomData<T>,

    ptr: *const T, // `*const` for variance (but this does not express ownership of a `T` *per se*)
    len: usize,
    cap: usize,
}
}

拥有分配的原始指针是一种如此普遍的模式,以至于标准库为自己创建了一个名为 Unique<T> 的实用程序,它:

  • 包装一个 *const T 用于变性
  • 包含一个 PhantomData<T>
  • 自动派生 Send/Sync,就像包含 T 一样
  • 将指针标记为 NonZero,用于空指针优化

PhantomData 模式表

这里是一个关于 PhantomData 所有奇妙使用方式的表格:

虚类型'a 的变性T 的变性Send/Sync
(或缺乏)
在 drop 胶水中悬空 'aT
例如#[may_dangle] Drop
PhantomData<T>-继承不允许(“拥有 T”)
PhantomData<&'a T>Send + Sync
需要
T : Sync
允许
PhantomData<&'a mut T>继承允许
PhantomData<*const T>-!Send + !Sync允许
PhantomData<*mut T>-!Send + !Sync允许
PhantomData<fn(T)>-Send + Sync允许
PhantomData<fn() -> T>-Send + Sync允许
PhantomData<fn(T) -> T>-Send + Sync允许
PhantomData<Cell<&'a ()>>-Send + !Sync允许
  • 注意:选择退出 Unpin 自动 trait 需要专用的 PhantomPinned 类型。