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>, } }
就是这样。生命周期将被绑定,并且你的迭代器将对 '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 在其析构函数中 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 胶水中悬空 'a 或 T (例如, #[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
类型。