Программирование RustВладение и заимствованиеRust-разработчик системного программного обеспечения

Разберите, какую операцию предотвращает Pin для типа без Unpin и зачем это нужно. Рассмотрите код: пример с...

Разберите, какую операцию предотвращает Pin<Box<T>> для типа без Unpin и зачем это нужно. Рассмотрите код:

use std::marker::PhantomPinned;

struct Task {
    value: String,
    _pin: PhantomPinned,
}

fn main() {
    let pinned = Box::pin(Task {
        value: String::from("ready"),
        _pin: PhantomPinned,
    });

    // let moved: Task = *pinned;
    println!("{}", pinned.value);
}
Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Для типа без Unpin Pin<Box<T>> запрещает переместить сам объект T через разыменование, потому что его адрес считается стабильным. При этом переместить сам указатель Box можно: перемещается владение указателем, а не объект, находящийся в куче.

Это нужно для типов, корректность которых зависит от неизменности адреса, например самоссылочных структур и некоторых состояний асинхронных вычислений.

Исторический контекст

Обычное владение Rust допускает перемещение значения: после перемещения объект может оказаться по другому адресу, но его логическое содержимое сохраняется. Для большинства типов это безопасно и удобно.

Однако существуют адресо-зависимые типы. Если такой объект хранит указатель или ссылку на собственную часть либо регистрирует свой адрес во внешней структуре, перемещение может сделать внутреннюю связь недействительной. Pin предоставляет стандартный контракт: после закрепления значение нельзя перемещать, если его тип не разрешает это через Unpin.

Постановка проблемы

В приведённом коде PhantomPinned делает Task не реализующим Unpin. Поэтому операция *pinned, которая попыталась бы извлечь Task из Pin<Box<Task>> по значению, запрещена: она переместила бы объект из выделенной области.

Если бы такой объект содержал указатель на собственное поле, после перемещения указатель продолжал бы указывать на старый адрес. Это могло бы привести к обращению к неверным данным, что нарушило бы гарантии безопасной памяти.

Подробное решение

Pin<P> закрепляет значение, доступное через указатель P. Для Pin<Box<T>> объект T находится в куче, а перемещение самого Pin<Box<T>> меняет только положение владельца указателя. Адрес T при этом не меняется.

Разыменование Pin<Box<T>> не даёт обычного владения T для типа T: !Unpin. Поэтому строка let moved: Task = *pinned; не компилируется. Получить обычную &Task можно, поскольку совместное чтение не перемещает объект.

use std::marker::PhantomPinned; use std::pin::Pin; struct Task { value: String, _pin: PhantomPinned, } fn read(task: Pin<&Task>) -> &str { &task.value } fn main() { let task = Box::pin(Task { value: String::from("ready"), _pin: PhantomPinned, }); println!("{}", read(task.as_ref())); // let moved: Task = *task; // ошибка: нельзя переместить !Unpin из Pin }

Unpin является маркерным свойством. Если тип реализует Unpin, закрепление не накладывает на него дополнительного запрета на перемещение: такой тип гарантирует, что перемещение не нарушит его инварианты. Поэтому Pin не означает абсолютную неизменность всех значений и не запрещает изменять поля через допустимый Pin<&mut T>.

Важно различать перемещение объекта и перемещение оболочки. let another = task; может переместить Pin<Box<Task>>, но не сам Task. Для доступа к полям также нельзя автоматически считать, что любое поле можно безопасно извлечь через Pin: проекция закреплённого значения должна сохранять инварианты типа. Если поле само адресо-зависимо, библиотека обычно предоставляет специальный безопасный метод проекции.

Pin не исправляет уже созданные неверные внутренние указатели и не делает произвольный код самоссылочной структуры безопасным. Он лишь формализует запрет на последующее перемещение; конструктор и методы типа всё равно должны корректно поддерживать его инварианты.

Ситуация из практики

Представим библиотечный объект асинхронной операции, который после инициализации хранит адрес собственного буфера. Планировщик перемещает значения между своими структурами, поэтому обычное хранение такого объекта по значению небезопасно: при перемещении сохранённый адрес перестанет соответствовать буферу.

Возможный вариант — всегда выделять объект в куче вручную и документировать запрет на перемещение. Это стабилизирует адрес, но оставляет инвариант на совести каждого вызывающего кода и усложняет API.

Другой вариант — использовать Pin<Box<T>> и сделать тип T: !Unpin. Тогда API явно передаёт гарантию стабильного адреса, а компилятор запрещает извлечь T перемещением через Pin. Этот вариант выбран: оболочку можно передавать планировщику, а сам объект остаётся на прежнем месте.

Что кандидаты часто упускают

  1. Вопрос: Запрещает ли Pin<Box<T>> перемещать сам Box?

    Ответ: Нет. Перемещение Pin<Box<T>> переносит владение указателем и не перемещает объект T в куче. Запрещено именно извлечение или иное перемещение T, если T: !Unpin.

  2. Вопрос: Всегда ли Pin<&mut T> делает T неподвижным?

    Ответ: Ограничение зависит от T. Для T: Unpin значение можно перемещать обычными безопасными операциями, потому что его корректность не зависит от адреса. Для T: !Unpin API Pin не позволяет безопасно получить владение значением через перемещение.

  3. Вопрос: Достаточно ли добавить PhantomPinned, чтобы самоссылочная структура стала безопасной?

    Ответ: Нет. PhantomPinned только предотвращает автоматическую реализацию Unpin. Нужно ещё корректно создать внутренние ссылки или указатели после размещения объекта, не допустить перемещения и предоставить методы, сохраняющие инварианты. Неверная инициализация указателя или неправильная проекция поля всё ещё может потребовать unsafe и привести к неопределённому поведению.