Перейти к содержанию

Заметки о Rust - PhantomData

Posted on:21 июня 2023 г. at 08:27

Example Dynamic OG Image link В этой статье сначала будут представлены «теоретические» концепции типа 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> не занимает места и выравнивается в одном байте, т.е.:

Тип PhantomData также тесно связан с правилом Drop-Check (dropck) и нестабильным атрибутом #[may_dangle]. Чтобы узнать больше это находится в RFC769, где dropck был введен и RFC1238 и RFC1327, где dropck был далее усовершенствован.

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

Интуитивно понятное объяснение и подробное описание работы PhantomData<T> в определении Vec<T> см. в этом ответе stackoverflow.

Ссылки