Тази публикация в блога първо ще представи „теоретичните“ концепции на типа Rust PhantomData<T> и след това ще проучи няколко примера от реалния свят, демонстриращи практическите му приложения.

какво е PhantomData<T>

Както е посочено в официалната документация, PhantomData<T> е тип с нулев размер (ZST), който не заема място и симулира присъствието на поле от даден тип T. Това е тип маркер, използван за предоставяне на информация на компилатора, която е полезна за целите на статичния анализ и е необходима за правилна „вариация“ и „проверка на пропускане“.

Като бърз пример е възможно да се дефинира структура като тази:

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>

За неизползваните параметри на продължителността на живота, нека разгледаме следната дефиниция на структурата Window:

use std::marker::PhantomData;

struct Window<'a, T: 'a> {
    start: *const T,
    end: *const T,
    phantom: PhantomData<&'a T>,
}

Полета start и end са необработени указатели. Те сочат към началото и края на прозорец от T стойности, но не носят никаква информация за целия живот. Това означава, че инструментът за проверка на заеми на Rust не може да ги използва за налагане на продължителността на живота 'a.

Поле phantom е PhantomData маркер, който носи продължителността на живота 'a. Това казва на инструмента за проверка на заеми на Rust, че структурата Window е логически свързана с данни от жизнения цикъл 'a, въпреки че всъщност не съхранява никакви препратки от тип &'a T.

Това гарантира, че данните, посочени от прозореца, няма да бъдат изпуснати, докато прозорецът все още се използва. Без PhantomData, Rust не би знаел за връзката през целия живот и не би могъл, например, да защити срещу грешки след използване. С други думи, PhantomData<&'a T> се използва, за да изрази, че Window се държи, тъй като има препратка към T с продължителност на живота 'a, което помага на Rust да наложи правилните правила за собственост и заемане.

Освен това, като допълнителна информация, имайте предвид, че тук Window стават ковариантни спрямо 'a и T.

Неизползвани параметри на типа и PhantomData<T>

В този случай PhantomData<T> се използва, за да посочи към какъв тип данни е „свързана“ структура:

struct ExternalResource<R> {
   resource_handle: *mut (),
   resource_type: PhantomData<R>,
}

Този случай възниква често при внедряване на външни функционални интерфейси (FFI). Обърнете се към пример за документация на стандартна библиотека за повече информация.

Примери от реалния свят на PhantomData<T>

Този раздел илюстрира няколко примера за използване в реалния свят на PhantomData<T>, взети директно от стандартната библиотека на Rust (представените кодови фрагменти се отнасят за Rust v1.70.0).

ЗаемиFd

BorrowedFd е заимстваната версия на OwnedFd (притежаван файлов дескриптор) и в стандартната библиотека е дефиниран в std/src/os/fd/owned.rs като:

pub struct BorrowedFd<'fd> {
    fd: RawFd,
    _phantom: PhantomData<&'fd OwnedFd>,
}

Тук полето PhantomData (_phantom) се използва, за да каже на компилатора Rust, че BorrowedFd е свързано с живота на OwnedFd, откъдето BorrowedFd е заимствано (въпреки че BorrowedFd всъщност не съдържа препратка към OwnedFd). Това е важно, за да се гарантира, че OwnedFd не се изпуска, докато BorrowedFd все още се използва.

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.

По-задълбочено обяснение защо PhantomData всъщност се изисква в Rc може да се намери в този отговор на StackOverflow и в този раздел на Rustonomicon.

Допълнителна информация

Този раздел предоставя няколко връзки към външни ресурси, които може да намерите за полезни, за да задълбочите знанията си за PhantomData.

В стандартната библиотека PhantomData се дефинира в core/src/marker.rs като:

pub struct PhantomData<T: ?Sized>;

Тъй като е тип с нулев размер (ZST), PhantomData<T> не заема място и е подравнен в един байт, т.е.:

  • size_of::<PhantomData<T>>() == 0
  • align_of::<PhantomData<T>>() == 1

Типът PhantomData също е тясно свързан с правилото Drop-Check (dropck) и атрибута #[may_dangle] unstable. За да научите повече за това, вижте RFC769, където dropck е въведен и RFC1238 и RFC1327, където dropck е допълнително прецизиран.

Имайте предвид също, че RFC1238 въвежда правила, които променят обстоятелствата, при които се изискват PhantomData<T> и #[may_dangle]. Например, те се използват в стандартната библиотека за прилагане на звучащ Vec<T>type, който не трябва да отговаря на прекалено ограничителното правило за проверка на пропускане (за допълнителна информация вижте специалния раздел в Rustonomicon)

За интуитивно обяснение и задълбочено гмуркане как PhantomData<T> работи в Vec<T> дефиниция, вижте този отговор на stackoverflow.

И накрая, друга интересна употреба на типа PhandomData е да се приложи „моделът тип състояние“, по-специално вариантът на параметъра тип състояние.

Препратки

0xor0ne