Rust中的PhantomData如何工作?

20

在Rust中,我发现PhantomData的概念非常令人困惑。我广泛地使用它来约束我的基于FFI的代码中的对象生命周期,但我仍然不确定我是否正确地使用了它。

这是一个编造的示例,展示我通常如何使用它。例如,我不希望MyStruct的实例超过Context的实例存在:

// FFI declarations and types
mod ffi {
    use std::ffi::c_void;
    pub type handle_t = *const c_void;
    // ...
}

// A wrapper structure for some context created and maintained
// inside the C library
struct Context {
    // ...
}

// Handle is only valid as long as the Context is alive.
// Hence, I use the PhantomData marker to constrain its lifetime.
struct MyStruct<'a> {
    marker: PhantomData<&'a Context>,
    handle: ffi::handle_t,
}

impl<'a> MyStruct<'a> {
    fn new(context: &'a Context) -> Self {
        let handle: ffi::handle_t = context.new_handle();
        MyStruct {
            marker: PhantomData,
            handle
        }
    }
}

fn main() {
    // Initialize the context somewhere inside the C library
    let ctx = Context::new(unsafe {ffi::create_context()});

    // Create an instance of MyStruct
    let my_struct = MyStruct::new(&ctx);

    // ...
}

我不太明白以下内容:

  1. marker: PhantomData 这个东西到底是什么,从语法上来看?我的意思是,它看起来不像一个构造器,我期望的应该是像 PhantomData{} 或者 PhantomData() 这样的形式。

  2. 为了进行生命周期跟踪,PhantomData 是否真的在乎 marker 声明中的实际类型?我尝试将其更改为 PhantomData<&'a usize>,但它仍然可以工作。

  3. 在我的 MyStruct::new() 方法的声明中,如果我忘记显式地指定 'a 生命周期的 context 参数,那么 PhantomData 的魔力就会消失,并且在 MyStruct 之前释放 Context 就变得可以了。这非常隐蔽; 编译器甚至没有发出警告。它给 marker 分配了什么生命周期,为什么?

  4. 与先前的问题相关;如果有多个输入参考参数具有潜在不同的生命周期,PhantomData 如何确定要使用哪个生命周期?


这个问题里有太多的问题了。 - mcarton
2
@mcarton,我觉得这些问题都源于对PhantomData的实际含义存在一个严重的误解,所以我把它们都放在了一个问题里。 - Roman Dmitrienko
Rustonomicon对这个问题有一个很好的回答:https://doc.rust-lang.org/nomicon/phantom-data.html - BallpointBen
1个回答

10
这个 marker: PhantomData 到底是什么语法结构?它看起来不像一个构造函数,我本来期望的是 PhantomData{} 或者 PhantomData() 这样的形式。
你可以定义一个零字段的结构体,像这样:
struct Foo;

并且可以像这样创建一个实例:

let foo: Foo = Foo;

类型和值都被命名为Foo

就生命周期跟踪而言,PhantomData是否在标记的声明中关心实际类型?我尝试将其更改为PhantomData<&'a usize>,它仍然可以工作。

PhantomData没有什么特别之处,除了它的类型参数未使用不会报错(参见源代码)。这种行为是通过#[lang = "phantom_data"]属性启用的,它只是编译器中的一个钩子。

在我的MyStruct::new()方法的声明中,如果我忘记显式指定'a生命周期用于context参数,那么PhantomData的魔力就会消失,并且可以在MyStruct之前删除Context。这是相当隐蔽的;编译器甚至不会发出警告。那么它分配给marker什么寿命,为什么呢? PhantomData存在的目的是让您向编译器提供它无法自行推断的信息,因为该信息涉及到您没有直接使用的类型。由您来提供正确的信息给编译器。

我不完全确定是否理解了这个问题。PhantomData并不会执行任何操作,它只是一种与编译器通信的方式,告诉它你正在以某种方式使用数据,并且你需要准确地表达这些信息。请注意,即使你错误地表达了约束条件,只有当你有unsafe代码时,才可能引入内存不安全性。在PhantomData中正确地表达生命周期,是创建围绕着不安全代码的安全抽象的一部分。


我之前不知道零字段结构体的这种语法,所以 "marker: PhantomData" 对我来说看起来像是一种“特殊”的语法,我认为 PhantomData 在编译器层面上有所作用。现在我明白了它其实更加简单和显而易见。谢谢! - Roman Dmitrienko
1
实际上,它不是特殊语法,而是语法糖:当您创建一个结构体 struct Foo; 时(而不是例如 struct Foo {}struct Foo();),它等价于 struct Foo {},除了编译器还会插入一个常量 const Foo: Foo = Foo {}; - jthulhu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接