В этой статье сначала будут представлены «теоретические» концепции типа 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
. В примере usesPhantomData<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
, но не содержат никакой информации о времени жизни. Это означает, что средство проверки заимствования Rust не может использовать их для обеспечения срока жизни.
Поле phantom
является маркером PhantomData
, который переносит время жизни. Это говорит Rust borrow checker, что структура Window
логически привязана к данным времени жизни 'a
, даже если он на самом деле не хранит никаких ссылок типа &' a T
.
Это гарантирует, что данные, указываемые Window
, не будут удалены, пока window
все еще используется. Без PhantomData
Rust не знал бы о жизненных отношениях и не мог бы, например, защитить от ошибок после использования. Другими словами, PhantomData<&'a T>
используется для выражения того, что Window
ведет себя так, так как имеет ссылку на T
с временем жизни 'a
, что помогает Rust применять правильные правила владения и заимствования.
Кроме того, в качестве дополнительной информации обратите внимание, что здесь Window
становится ковариантным над 'а
и Т
.
Неиспользуемые параметры типа и PhantomData<T>
В этом случае PhantomData<T>
используется для указания типа данных, к которым «привязана» структура:
struct ExternalResource<R> {
resource_handle: *mut (),
resource_type: PhantomData<R>,
}
Этот случай часто возникает при реализации внешних функциональных интерфейсов (FFI). Дополнительные сведения см. в примере стандартной библиотечной документации.
Реальные примеры PhantomData<T>
Этот раздел иллюстрирует несколько реальных примеров использования PhantomData<T>
, взятых непосредственно из стандартной библиотеки Rust (представленные фрагменты кода относятся к Rust v1.70.0).
BorrowedFd
BoretwedFd
- это заимствованная версия Fd
(дескриптор файла, принадлежащий владельцу), и в стандартной библиотеке она определена в std/src/os/fd/owned.rs
как:
pub struct BorrowedFd<'fd> {
fd: RawFd,
_phantom: PhantomData<&'fd OwnedFd>,
}
Здесь поле PhantomData
(_phantom
) используется для того, чтобы сообщить компилятору Rust, что BorreadWedFd
привязан к сроку жизни BreamedFd
, откуда был заимствован BoretWedFd
(даже если BorewedFd
фактически не содержит ссылки на BoreFd
). Это важно для обеспечения того, чтобы функция ForwerwedFd
не удалялась, в то время как функция BorreadWedFd
все еще используется.
Iter<T>
Итератор по срезу [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].
Чтобы узнать больше это находится в RFC769, где dropck
был введен и RFC1238 и RFC1327, где dropck
был далее усовершенствован.
Следует также отметить, что RFC1238 введены правила, которые изменили условия, при которых требуются PhantomData<T>
и #[may_dangle]
. Например, они используются в стандартной библиотеке для реализации типа Vec<T>
, который не должен соответствовать слишком ограничительному правилу проверки удаления (дополнительную информацию см. в специальном разделе в Rustonomicon).
Интуитивно понятное объяснение и подробное описание работы PhantomData<T>
в определении Vec<T>
см. в этом ответе stackoverflow.
Ссылки
- Rust standard library documentation on
- PhantomData определение
- BorrowedFd определение
- Iter определение
- Subtyping and Variance
- Drop Check
- Rc определение
- PhantomData и Rc (StackOverflow ответ)
- PhantomData и may_dangle (Rustonomicon)
- Typestate pattern
Vec<T>
и PhantomData- RFC769: sound generic drop
- RFC1238: nonparametric dropck
- RFC1327: dropck param eyepatch
- Generic parameters, Drop Checking и may_dangle (Rustonomicon)
- Unused type parameters
PhantomData<T>