• Rust中的PhantomData<T>类型
  • 发布于 2个月前
  • 176 热度
    0 评论
这篇文章将首先介绍Rust PhantomData<T>类型的“理论”概念,然后探索一些其实际应用的真实示例。

PhantomData<T>是什么?
正如官方文档中所述,PhantomData<T>是一个零大小类型(Zero size Type, ZST),它不占用空间,并模拟存在给定的类型T。它是一个标记类型,用于向编译器提供对静态分析有用的信息,并且对于正确的类型协变和drop检查也是必要的。

看一个简单的例子,可以这样定义一个结构体:
struct PdStruct<T> {
    data: i32,
    pd: PhantomData<T>,
}
在这个例子中,字段pd(其类型为PhantomData<T>)不会增加结构体PdStruct<T>的大小,而是告诉编译器将PdStruct<T>视为拥有T,尽管后者实际上并没有在结构体本身中使用。因此,编译器知道当PdStruct<T>类型的值被删除时,T也可能被删除。PhantomData<T>通常用于原始指针、未使用的生命周期参数和未使用的类型参数。下面分别是这三种情况的示例。

原始指针和PhantomData<T>
看下面的代码:
use std::marker::PhantomData;

struct MyRawPtrStruct<T> {
    ptr: *mut T,
    _marker: PhantomData<T>,
}

impl<T> MyRawPtrStruct<T> {
    fn new(t: T) -> MyRawPtrStruct<T> {
        let t = Box::new(t);
        MyRawPtrStruct {
            ptr: Box::into_raw(t),
            _marker: PhantomData,
        }
    }
}
在这个例子中,MyRawPtrStruct是一个简单的智能指针,拥有一个堆分配的T。Rust编译器不能自动推断原始指针ptr的生命周期或所有权细节。这个例子使用了PhantomData<T>来表达MyRawPtrStruct拥有一个T的事实,即使T实际上并没有出现在结构体中(它在一个原始指针后面)。这有助于Rust编译器正确地推断删除顺序和其他与所有权相关的属性。

未使用的生命周期参数和PhandomData<T>

我们看一下这个例子:
use std::marker::PhantomData;
struct Window<'a, T: 'a> {
    start: *const T,
    end: *const T,
    phantom: PhantomData<&'a T>,
}
字段start和end是原始指针,它们指向带T值的Window开始和结束,但它们不携带任何生命周期信息。这意味着Rust的借用检查器不能使用它们来指定生命周期'a。字段phantom是一个带有生命周期'a的PhantomData标记。这告诉Rust的借用检查器,Window结构体在逻辑上与生命周期'a的数据绑定在一起,即使它实际上并没有存储任何类型的引用。

这确保了当Window仍在使用时,Window所指向的数据不会被丢弃。如果没有PhantomData, Rust就不会知道它们的生命周期关系,也无法防止use-after-free的bug。换句话说,PhantomData<&'a T>被用来表达Window的行为,就像它有一个生命周期为'a的T的引用,这有助于Rust强制执行正确的所有权和借用规则。

另外,作为附加信息,注意这里的Window在'a和T上是协变的。未使用的类型参数和PhantomData<T>

在这种情况下,PhantomData<T>用于指示结构体“绑定”特定的数据类型:
struct ExternalResource<R> {
   resource_handle: *mut (),
   resource_type: PhantomData<R>,
}
这种情况在实现外部函数接口(ffi)时经常出现。有关更多信息,请参阅标准库文档示例。

PhantomData<T>的实际例子
下面演示了几个直接取自Rust标准库的PhantomData<T>的实际示例(所提供的代码片段参考Rust v1.70.0)。
BorrowedFd
BorrowedFd是OwnedFd(拥有的文件描述符)的借用版本,在标准库中,它定义在std/src/os/fd/owned.rs中:
pub struct BorrowedFd<'fd> {
    fd: RawFd,
    _phantom: PhantomData<&'fd OwnedFd>,
}
在这里,PhantomData字段(_phantom)用于告诉Rust编译器,BorrowedFd与借来的OwnedFd的生命周期相关联(即使BorrowedFd实际上并不持有对OwnedFd的引用)。这对于确保在BorrowedFd仍在使用时OwnedFd不会被删除,这非常重要。

Iter<T>
基于Slice [T]的迭代器在标准库core/src/slice/iter.rs中定义:
pub struct Iter<'a, T: 'a> {
    ptr: NonNull<T>,
    end: *const T,
    _marker: PhantomData<&'a T>,
}
在这种情况下,PhantomData<&'a T>用于指示结构体Iter与生命周期'a绑定。这很重要,因为它告诉Rust编译器Iter不能超过它可能对T的引用(数据T仅由两个原始指针ptr和end指向,它们不携带生命周期信息)。这对Rust保证内存安全至关重要。

Rc<T>
最后一个例子,我们看一下Rc<T>,在标准库在alloc/src/rc.rs中定义。包含一个PhantomData字段:
pub struct Rc<T: ?Sized> {
    ptr: NonNull<RcBox<T>>,
    phantom: PhantomData<RcBox<T>>,
}
这里使用PhantomData告诉删除检查器,删除Rc<T>可能会导致类型为T的值被删除。
用户评论