Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

4. PhantomData — типы, которые не несут данных 🔴

Что вы узнаете:

  • Зачем нужен PhantomData<T> и три проблемы, которые он решает
  • Маркировка времён жизни (lifetime branding) для проверки области действия на этапе компиляции
  • Паттерн «единицы измерения» для арифметики с проверкой размерностей
  • Вариантность (ковариантность, контравариантность, инвариантность) и то, как ею управляет PhantomData

Что решает PhantomData

PhantomData<T> — это тип нулевого размера, который сообщает компилятору: «эта структура логически связана с T, хотя самого T она не содержит». Он влияет на вариантность, проверку drop и автоматический вывод auto-трейтов, не занимая при этом памяти.

#![allow(unused)]
fn main() {
use std::marker::PhantomData;

// Без PhantomData:
struct Slice<'a, T> {
    ptr: *const T,
    len: usize,
    // Проблема: компилятор не знает, что эта структура заимствует данные из 'a,
    // и не учитывает связь с T при проверке drop
}

// С PhantomData:
struct Slice<'a, T> {
    ptr: *const T,
    len: usize,
    _marker: PhantomData<&'a T>,
    // Теперь компилятор знает:
    // 1. Эта структура заимствует данные с временем жизни 'a
    // 2. Она ковариантна по 'a (времена жизни могут сокращаться)
    // 3. Проверка drop учитывает T
}
}

Три задачи PhantomData:

ЗадачаПримерЧто делает
Привязка времени жизниPhantomData<&'a T>Структура считается заимствующей 'a
Имитация владенияPhantomData<T>Проверка drop считает, что структура владеет T
Управление вариантностьюPhantomData<fn(T)>Делает структуру контравариантной по T

Маркировка времени жизни (lifetime branding)

Используйте PhantomData, чтобы не допустить смешивания значений из разных «сессий» или «контекстов»:

use std::cell::RefCell;
use std::marker::PhantomData;

/// Дескриптор, привязанный к конкретному экземпляру арены.
/// Инвариантен по 'arena — не позволяет использовать дескриптор одной арены с другой.
struct ArenaHandle<'arena> {
    index: usize,
    _brand: PhantomData<*mut &'arena ()>,
}

/// Арена, которая помечает каждый дескриптор своим уникальным временем жизни.
struct Arena<'arena> {
    data: RefCell<Vec<String>>,
    _phantom: PhantomData<&'arena ()>,
}

/// Создаёт арену и передаёт её в замыкание.
/// Каждый вызов получает уникальное непрозрачное время жизни, которое нельзя подделать.
fn with_arena<R>(f: impl for<'arena> FnOnce(&Arena<'arena>) -> R) -> R {
    let arena = Arena {
        data: RefCell::new(Vec::new()),
        _phantom: PhantomData,
    };
    f(&arena)
}

impl<'arena> Arena<'arena> {
    /// Выделяет строку и возвращает дескриптор с маркером арены
    fn alloc(&self, value: String) -> ArenaHandle<'arena> {
        let mut data = self.data.borrow_mut();
        let index = data.len();
        data.push(value);
        ArenaHandle { index, _brand: PhantomData }
    }

    /// Поиск по дескриптору: принимает только дескрипторы ЭТОЙ арены
    fn get(&self, handle: &ArenaHandle<'arena>) -> String {
        let data = self.data.borrow();
        data[handle.index].clone()
    }
}

fn main() {
    with_arena(|arena1| {
        let handle1 = arena1.alloc("hello".to_string());
        println!("{}", arena1.get(&handle1)); // ✅

        // Нельзя использовать handle1 с другой ареной — ошибка на этапе компиляции
        // with_arena(|arena2| {
        //     arena2.get(&handle1); // ❌ заимствованные данные выходят за пределы замыкания
        // });
    });
}

Паттерн «единицы измерения»

Предотвращайте смешивание несовместимых единиц на этапе компиляции без затрат во время выполнения:

use std::marker::PhantomData;
use std::ops::{Add, Mul};

// Маркерные типы единиц (нулевого размера)
struct Meters;
struct Seconds;
struct MetersPerSecond;

#[derive(Debug, Clone, Copy)]
struct Quantity<Unit> {
    value: f64,
    _unit: PhantomData<Unit>,
}

impl<U> Quantity<U> {
    fn new(value: f64) -> Self {
        Quantity { value, _unit: PhantomData }
    }
}

// Складывать можно только одинаковые единицы:
impl<U> Add for Quantity<U> {
    type Output = Quantity<U>;
    fn add(self, rhs: Self) -> Self::Output {
        Quantity::new(self.value + rhs.value)
    }
}

// Метры / секунды = метры в секунду (собственная реализация трейта)
impl std::ops::Div<Quantity<Seconds>> for Quantity<Meters> {
    type Output = Quantity<MetersPerSecond>;
    fn div(self, rhs: Quantity<Seconds>) -> Quantity<MetersPerSecond> {
        Quantity::new(self.value / rhs.value)
    }
}

fn main() {
    let dist = Quantity::<Meters>::new(100.0);
    let time = Quantity::<Seconds>::new(9.58);
    let speed = dist / time; // Quantity<MetersPerSecond>
    println!("Скорость: {:.2} м/с", speed.value); // 10.44 м/с

    // let nonsense = dist + time; // ❌ Compile error: can't add Meters + Seconds
}

Это чистая магия системы типов: PhantomData<Meters> имеет нулевой размер, поэтому Quantity<Meters> имеет ту же раскладку в памяти, что и f64. Во время выполнения никаких накладных расходов на обёртку, но полная проверка единиц на этапе компиляции.

PhantomData и проверка drop

Когда компилятор проверяет, не может ли деструктор структуры обратиться к уже недействительным данным, он использует PhantomData, чтобы принять решение:

#![allow(unused)]
fn main() {
use std::marker::PhantomData;

// PhantomData<T>: компилятор считает, что мы МОЖЕМ удалить T
// Это значит, что T должен жить дольше нашей структуры
struct OwningSemantic<T> {
    ptr: *const T,
    _marker: PhantomData<T>,  // «Логически я владею T»
}

// PhantomData<*const T>: компилятор считает, что мы НЕ владеем T
// Более либерально: T не обязан жить дольше нас
struct NonOwningSemantic<T> {
    ptr: *const T,
    _marker: PhantomData<*const T>,  // «Я лишь указываю на T»
}
}

Практическое правило: при обёртывании сырых указателей выбирайте PhantomData внимательно:

  • Пишете контейнер, который владеет своими данными? → PhantomData<T>
  • Пишете тип-представление (view) или ссылку? → PhantomData<&'a T> или PhantomData<*const T>

Вариантность: почему важен параметр типа в PhantomData

Вариантность определяет, можно ли подставить вместо обобщённого типа его подтип или надтип (в Rust «подтип» означает «время жизни длиннее»). Ошибка в вариантности приводит либо к отклонению корректного кода, либо к принятию небезопасного.

graph LR
    subgraph Ковариантность
        direction TB
        A1["&'long T"] -->|"может стать"| A2["&'short T"]
    end

    subgraph Контравариантность
        direction TB
        B1["fn(&'short T)"] -->|"может стать"| B2["fn(&'long T)"]
    end

    subgraph Инвариантность
        direction TB
        C1["&'a mut T"] ---|"подстановка НЕ допускается"| C2["&'b mut T"]
    end

    style A1 fill:#d4efdf,stroke:#27ae60,color:#000
    style A2 fill:#d4efdf,stroke:#27ae60,color:#000
    style B1 fill:#e8daef,stroke:#8e44ad,color:#000
    style B2 fill:#e8daef,stroke:#8e44ad,color:#000
    style C1 fill:#fadbd8,stroke:#e74c3c,color:#000
    style C2 fill:#fadbd8,stroke:#e74c3c,color:#000

Три вариантности

ВариантностьСмысл«Можно ли подставить…»Пример в Rust
КовариантностьПодтип передаётся по направлению'long там, где ожидается 'short ✅&'a T, Vec<T>, Box<T>
КонтравариантностьПодтип передаётся против направления'short там, где ожидается 'long ✅fn(T) (в позиции параметра)
ИнвариантностьПодстановка не допускаетсяНи в одну сторону ✅&mut T, Cell<T>, UnsafeCell<T>

Почему &'a T ковариантна по 'a

fn print_str(s: &str) {
    println!("{s}");
}

fn main() {
    let owned = String::from("hello");
    // owned живёт всё время выполнения функции ('long)
    // print_str ожидает &'_ str ('short: только на время вызова)
    print_str(&owned); // ✅ Ковариантность: 'long → 'short безопасно
    // Ссылка с более долгим временем жизни всегда может использоваться там, где нужна более короткая.
}

Почему &mut T инвариантна по T

#![allow(unused)]
fn main() {
// Если бы &mut T была ковариантна по T, этот код скомпилировался бы:
fn evil(s: &mut &'static str) {
    // Мы могли бы записать в слот &'static str ссылку с более коротким временем жизни!
    let local = String::from("temporary");
    // *s = &local; // ← Создал бы висячий &'static str
}

// Инвариантность запрещает это: &'static str ≠ &'a str при мутации.
// Компилятор полностью отклоняет подстановку.
}

Как PhantomData управляет вариантностью

PhantomData<X> придаёт вашей структуре ту же вариантность, что и у X:

#![allow(unused)]
fn main() {
use std::marker::PhantomData;

// Ковариантна по 'a: Ref<'long> можно использовать как Ref<'short>
struct Ref<'a, T> {
    ptr: *const T,
    _marker: PhantomData<&'a T>,  // Ковариантна по 'a, ковариантна по T
}

// Инвариантна по T: предотвращает небезопасное сокращение времени жизни T
struct MutRef<'a, T> {
    ptr: *mut T,
    _marker: PhantomData<&'a mut T>,  // Ковариантна по 'a, ИНВАРИАНТНА по T
}

// Контравариантна по T: полезна для контейнеров callback-функций
struct CallbackSlot<T> {
    _marker: PhantomData<fn(T)>,  // Контравариантна по T
}
}

Шпаргалка по вариантности PhantomData:

Тип PhantomDataВариантность по TВариантность по 'aКогда использовать
PhantomData<T>Ковариантна—Вы логически владеете T
PhantomData<&'a T>КовариантнаКовариантнаВы заимствуете T со временем жизни 'a
PhantomData<&'a mut T>ИнвариантнаКовариантнаВы заимствуете T изменяемо
PhantomData<*const T>Ковариантна—Невладеющий указатель на T
PhantomData<*mut T>Инвариантна—Невладеющий изменяемый указатель
PhantomData<fn(T)>Контравариантна—T находится в позиции аргумента
PhantomData<fn() -> T>Ковариантна—T находится в позиции возвращаемого значения
PhantomData<fn(T) -> T>Инвариантна—T в обеих позициях: ковариантность и контравариантность уравновешивают друг друга

Пример: почему это важно на практике

use std::marker::PhantomData;

// Токен, который помечает значения временем жизни сессии.
// ДОЛЖЕН быть ковариантен по 'a, иначе вызывающий код не сможет сократить
// время жизни при передаче в функции, которым нужно более короткое заимствование.
struct SessionToken<'a> {
    id: u64,
    _brand: PhantomData<&'a ()>,  // ✅ Ковариантна: вызывающий код может сократить 'a
    // _brand: PhantomData<fn(&'a ())>,  // ❌ Контравариантна: ломает эргономику
    // _brand: PhantomData<&'a mut ()>;  // По-прежнему ковариантна по 'a (инвариантна по T, но T фиксирован как ())
}

fn use_token(token: &SessionToken<'_>) {
    println!("Используем токен {}", token.id);
}

fn main() {
    let token = SessionToken { id: 42, _brand: PhantomData };
    use_token(&token); // ✅ Работает, потому что SessionToken ковариантна по 'a
}

Правило выбора: начинайте с PhantomData<&'a T> (ковариантна). Переходите к PhantomData<&'a mut T> (инвариантна), только если ваша абстракция выдаёт изменяемый доступ к T. PhantomData<fn(T)> (контравариантна) почти никогда не нужна: она корректна только в сценариях хранения callback-функций.

Ключевые выводы: PhantomData

  • PhantomData<T> несёт информацию о типе и времени жизни без затрат во время выполнения
  • Используйте его для маркировки времён жизни, управления вариантностью и паттерна единиц измерения
  • Проверка drop: PhantomData<T> сообщает компилятору, что ваш тип логически владеет T

См. также: гл. 3 — Newtype и type-state — паттерны type-state, которые используют PhantomData. гл. 12 — Unsafe Rust — о том, как PhantomData взаимодействует с сырыми указателями.


Упражнение: единицы измерения с PhantomData ★★ (~30 минут)

Расширьте паттерн единиц измерения, чтобы он поддерживал:

  • Meters, Seconds, Kilograms
  • сложение одинаковых единиц
  • умножение: Meters * Meters = SquareMeters
  • деление: Meters / Seconds = MetersPerSecond
🔑 Решение
use std::marker::PhantomData;
use std::ops::{Add, Mul, Div};

#[derive(Clone, Copy)]
struct Meters;
#[derive(Clone, Copy)]
struct Seconds;
#[derive(Clone, Copy)]
struct Kilograms;
#[derive(Clone, Copy)]
struct SquareMeters;
#[derive(Clone, Copy)]
struct MetersPerSecond;

#[derive(Debug, Clone, Copy)]
struct Qty<U> {
    value: f64,
    _unit: PhantomData<U>,
}

impl<U> Qty<U> {
    fn new(v: f64) -> Self { Qty { value: v, _unit: PhantomData } }
}

impl<U> Add for Qty<U> {
    type Output = Qty<U>;
    fn add(self, rhs: Self) -> Self::Output { Qty::new(self.value + rhs.value) }
}

impl Mul<Qty<Meters>> for Qty<Meters> {
    type Output = Qty<SquareMeters>;
    fn mul(self, rhs: Qty<Meters>) -> Qty<SquareMeters> {
        Qty::new(self.value * rhs.value)
    }
}

impl Div<Qty<Seconds>> for Qty<Meters> {
    type Output = Qty<MetersPerSecond>;
    fn div(self, rhs: Qty<Seconds>) -> Qty<MetersPerSecond> {
        Qty::new(self.value / rhs.value)
    }
}

fn main() {
    let width = Qty::<Meters>::new(5.0);
    let height = Qty::<Meters>::new(3.0);
    let area = width * height; // Qty<SquareMeters>
    println!("Площадь: {:.1} м²", area.value);

    let dist = Qty::<Meters>::new(100.0);
    let time = Qty::<Seconds>::new(9.58);
    let speed = dist / time;
    println!("Скорость: {:.2} м/с", speed.value);

    let sum = width + height; // Одинаковые единицы ✅
    println!("Сумма: {:.1} м", sum.value);

    // let bad = width + time; // ❌ Compile error: can't add Meters + Seconds
}