Rust程式語言有提供泛型和生命周期機制,在定義列舉或是結構體的時候,可以利用泛型來指定結構體的欄位型別,使我們在程式撰寫階段不必明確地限制住這個結構體到底能儲存哪些型別的資料。如果要進一步的限制泛型型別參數所代表的型別範圍,可以替泛型型別參數加上特性的限制,使其所對應的型別必須要有實作指定的特性。但是,當特性有使用到泛型生命周期參數的話,就可能會遇到一些問題了。



例如現在有個特性A,它有一個泛型生命周期參數'a,可以將符合這個生命周期參數的字串切片回傳出來。

trait A<'a> {
    fn as_str(&'a self) -> &'a str;
}

然後我們希望有個結構體B,可以儲存實作了這個特性A的型別實體,因此我們會很直覺地如以下這樣定義結構體B

struct B<'a, T: A<'a>> {
    s: T
}

但是以上程式在編譯階段時會出現問題,因為編譯器並不知道泛型生命周期參數'a到底是什麼,因為它在結構體B的資料欄位中並沒有被使用到。這部份很奇怪,資料欄位怎麼會沒有使用到泛型生命周期參數'a呢?泛型型別參數T不就表明了其所代表的型別必須要實作A<'a>特性嗎?

在一開始筆者以為這個Rust程式語言的Bug,所以都先將結構體改用以下的方式定義來暫時解決這個問題:

struct B<'a, T: A<'a>> {
    s: T,
    _not_used: &'a str,
}

如果是列舉的話就會長這樣:

enum C<'a, T: A<'a>> {
    V1(T),
    _NotUsed(&'a str),
}

這樣的作法雖然可以解決編譯的問題,卻會導致列舉或是結構體的型別大小變大,因為它必須要多儲存一個&'a str參考,並且也不是說在所有情況下都適用這樣的解決方案。

隨著後來愈來愈了解Rust程式語言後,就發現標準函式庫的marker模組有提供一個PhantomData結構體,就是專門用來解決這個問題的。

PhantomData結構體有一個泛型型別參數T,這個T具體是什麼型別並沒有任何限制,且PhantomData結構體本身是不佔空間的(zero-sized)。將以上的結構體和列舉改用PhantomData結構體來定義的話,就會變成以下這樣:

use std::marker::PhantomData;

struct B<'a, T: A<'a>> {
    s: T,
    _not_used: PhantomData<&'a str>,
}

enum C<'a, T: A<'a>> {
    V1(T),
    _NotUsed(PhantomData<&'a str>),
}

如果要更好看一點,可以寫成這樣:

use std::marker::PhantomData;

struct B<'a, T: A<'a>> {
    s: T,
    _not_used: PhantomData<&'a T>,
}

enum C<'a, T: A<'a>> {
    V1(T),
    _NotUsed(PhantomData<&'a T>),
}

我們不用擔心具體要給PhantomData的資料欄位什麼值,通通傳入PhantomData就好了。什麼意思呢?直接看程式碼吧!

impl<'a, T: A<'a>> B<'a, T> {
    fn new(s: T) -> Self {
        B {
            s,
            _not_used: PhantomData,
        }
    }
}

以上程式中,我們在建立結構體B的實體時,在PhantomData的資料欄位中,直接指派了PhantomData給它。

如果是列舉的話,我們甚至可以不必管PhantomData的資料欄位。如下:

impl<'a, T: A<'a>> C<'a, T> {
    fn v1(s: T) -> Self {
        C::V1(s)
    }
}

所以說,Rust程式語言中所謂的幽靈資料(PhantomData)其實就是作為列舉或結構體中的資料欄位,純粹為了要給編譯器看所設計的資料型別啦!幽靈資料除了能處理上述的有用到泛型生命周期參數的特性外,也可以處理有用到泛型型別參數的特性。

來複雜化一下上述的例子,將特性A的定義改成以下這樣:

trait A<'a, K> {
    fn as_type(&'a self) -> &'a K;
}

結構體B的定義改成以下這樣:

struct B<'a, K, T: A<'a, K>> {
    s: T,
    _not_used: PhantomData<&'a T>,
}

編譯時會發現結構體B編譯失敗,原因在於它的資料欄位中並沒有用到泛型型別參數K,此時的解法很簡單,我們已經會了,只要想辦法將我們沒用到的泛型參數都塞給PhantomData的資料欄位來用就好了。於是乎,修改後的結構體B的程式碼如下:

struct B<'a, K, T: A<'a, K>> {
    s: T,
    _not_used: PhantomData<&'a K>,
}