Тази публикация в блога първо ще представи „теоретичните“ концепции на типа 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
е да се приложи „моделът тип състояние“, по-специално вариантът на параметъра тип състояние.
Препратки
- Документация на стандартната библиотека Rust на
PhantomData<T>
PhantomData
дефиницияBorrowedFd
дефиницияIter
дефиниция- „Подтипове и вариация“
- „Дроп проверка“
Rc
дефиницияPhantomData
иRc
(Отговор на StackOverflow)PhantomData
иmay_dangle
(Рустономикон)- „Модел на състояние на типа“
Vec<T>
иPhantomData
- RFC769: звук, общ спад
- RFC1238: непараметричен dropck
- RFC1327: dropck param eyepatch
- Общи параметри, Drop Checking и
may_dangle
(Rustonomicon) - Неизползвани параметри на типа (
PhantomData<T>
)