PhantomData
在使用不安全代码时,我们经常会遇到这样的情况:类型或生命周期在逻辑上与结构体相关联,但实际上并不是字段的一部分。这最常发生在生命周期中。例如,`&'a [T]` 的 `Iter` 大致定义如下
#![allow(unused)] fn main() { struct Iter<'a, T: 'a> { ptr: *const T, end: *const T, } }
然而,由于 `'a` 未在结构体的正文中使用,因此它是*无界*的。由于这在历史上造成的问题,无界生命周期和类型在结构体定义中是被*禁止*的。因此,我们必须以某种方式在正文中引用这些类型。正确地做到这一点对于拥有正确的变型和 Drop 检查至关重要。
我们使用 `PhantomData` 来做到这一点,它是一种特殊的标记类型。`PhantomData` 不占用空间,但为了静态分析的目的,它模拟了给定类型的字段。这被认为比明确告诉类型系统你想要的变型类型更不容易出错,同时也提供了其他有用的东西,比如自动 trait 和 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>, } }
就是这样。生命周期将是有界的,并且你的迭代器将在 `'a` 和 `T` 上是协变的。一切正常。
泛型参数和 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 在其析构函数中丢弃任何 T 来确定 Drop 检查的健全性。这反过来又会允许人们使用 Vec 的析构函数来创建不健全的行为。
为了告诉 Drop 检查器我们*确实*拥有 T 类型的,因此当*我们*丢弃时可能会丢弃一些 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 类型的),因此如果 `Vec<T>` 被丢弃,Rust 将不允许它们*悬空*。
当一个类型已经有一个 `Drop impl` 时,添加一个额外的 `_owns_T: PhantomData<T>` 字段是*多余的*,没有任何意义,就 Drop 检查而言(它仍然会影响变型和自动 trait)。
- (高级边缘情况:如果包含 `PhantomData` 的类型根本没有 `Drop` impl,但仍然有 Drop 胶水(通过拥有*另一个*具有 Drop 胶水的字段),那么这里提到的 Drop 检查/`#[may_dangle]` 注意事项也适用:`PhantomData<T>` 字段将要求 T 在包含类型超出范围时是可丢弃的)。
但这种情况有时会导致代码过于严格。这就是为什么标准库使用一个不稳定的 `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>` 的 `'s` 生命周期引用 `str`ing 的向量,但在 `let s: String` 的情况下,它在 `Vec` 之前被丢弃,因此在 `Vec` 被丢弃时,`'s` 已经*过期*,并且使用了 `impl<'s> Drop for Vec<&'s str> {`。
这意味着,如果要使用这样的 `Drop`,它将处理一个*过期*或*悬空*的生命周期 `'s`。但这与 Rust 的原则相悖,在 Rust 中,默认情况下,函数签名中涉及的所有 Rust 引用都是非悬空的,并且可以安全地解引用。
因此,Rust 必须保守地拒绝这段代码。
然而,在真实的 `Vec` 的情况下,`Drop` impl 并不关心 `&'s str`,*因为它本身没有 Drop 胶水*:它只想释放底层缓冲区。
换句话说,如果上面的代码片段能够以某种方式被接受,那就太好了,可以通过对 Vec
进行特殊处理,或者依赖于 Vec
的某些特殊属性:Vec
可以尝试*承诺在被丢弃时不使用它持有的 &'s str
*。
这是一种可以使用 #[may_dangle]
表达的 unsafe
承诺
unsafe impl<#[may_dangle] 's> Drop for Vec<&'s str> { /* … */ }
或者,更一般地说
unsafe impl<#[may_dangle] T> Drop for Vec<T> { /* … */ }
是选择退出 Rust 的丢弃检查器对被丢弃实例的类型参数不允许悬空的保守假设的 unsafe
方式。
当这样做时,例如在标准库中,我们需要在 T
本身具有丢弃胶水的情况下小心。在这种情况下,想象一下用 struct PrintOnDrop<'s> /* = */ (&'s str);
替换 &'s str
,它将具有一个 Drop
实现,其中内部的 &'s str
将被解引用并打印到屏幕上。
实际上,Drop for Vec<T> {
在释放后备缓冲区之前,确实必须在 T
项目具有丢弃胶水时传递地丢弃每个 T
项目;在 PrintOnDrop<'s>
的情况下,这意味着 Drop for Vec<PrintOnDrop<'s>>
必须在释放后备缓冲区之前传递地丢弃 PrintOnDrop<'s>
元素。
因此,当我们说 's
#[may_dangle]
时,这是一个过于宽松的说法。我们宁愿说:“'s
可以悬空,前提是它不涉及某些传递丢弃胶水”。或者,更一般地说,“T
可以悬空,前提是它不涉及某些传递丢弃胶水”。每当*我们拥有一个 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 (或缺乏 thereof) | 丢弃胶水中的悬空 'a 或 T (*例如*, #[may_dangle] Drop ) |
---|---|---|---|---|
PhantomData<T> | - | **协变** | 继承 | 不允许(“拥有 T ”) |
PhantomData<&'a T> | **协变** | **协变** | 发送 + 同步 需要 T : 同步 | 允许 |
PhantomData<&'a mut T> | **协变** | **逆变** | 继承 | 允许 |
PhantomData<*const T> | - | **协变** | !发送 + !同步 | 允许 |
PhantomData<*mut T> | - | **逆变** | !发送 + !同步 | 允许 |
PhantomData<fn(T)> | - | **逆变** | 发送 + 同步 | 允许 |
PhantomData<fn() -> T> | - | **协变** | 发送 + 同步 | 允许 |
PhantomData<fn(T) -> T> | - | **逆变** | 发送 + 同步 | 允许 |
PhantomData<Cell<&'a ()>> | **逆变** | - | 发送 + !同步 | 允许 |
- 注意:选择退出
Unpin
自动特征需要专用的PhantomPinned
类型。