Объясните механизм, благодаря которому UnsafeCell<T> разрешает внутреннюю изменяемость без снятия всех правил алиасинга Rust.
UnsafeCell<T> — единственный базовый механизм Rust, который сообщает компилятору: значение типа T внутри этой оболочки может изменяться даже при наличии общей ссылки на оболочку. Он отменяет только гарантию неизменности внутреннего T, но не отменяет требования корректного алиасинга, времени жизни и отсутствия гонок данных.
Сам по себе UnsafeCell не делает операции безопасными. Безопасный тип поверх него обязан самостоятельно доказать, что одновременные доступы к внутреннему значению допустимы.
Модель владения Rust обычно трактует &T как общую неизменяемую ссылку: пока такая ссылка существует, значение не должно изменяться. Это позволяет компилятору выполнять оптимизации и проверять отсутствие конфликтующих доступов.
Однако системные и библиотечные типы часто требуют общей ручки с изменяемым состоянием: счётчика, кэша, ленивой инициализации или объекта с динамической проверкой заимствований. UnsafeCell был введён как узкая граница, позволяющая выразить это исключение, не отключая модель алиасинга для всех остальных типов.
На этой основе построены Cell, RefCell, Mutex и другие типы внутренней изменяемости. Они добавляют собственные правила доступа поверх примитива UnsafeCell.
Если тип хранит обычный T, компилятор вправе считать значение за общей ссылкой неизменным. Простое хранение raw pointer внутри структуры не сообщает компилятору, что такие предположения нужно изменить: указатель сам по себе не является корректным описанием модели алиасинга.
Если же тип использует UnsafeCell, его реализация получает возможность изменить внутреннее значение через общую ссылку. Но ошибка в реализации может привести к неопределённому поведению: например, к созданию одновременно используемых конфликтующих ссылок, чтению неинициализированной памяти, use-after-free или гонке данных.
У UnsafeCell<T> есть метод get, возвращающий *mut T. Через этот указатель реализация может изменять значение, находящееся за общей ссылкой на UnsafeCell<T>. Специальная семантика UnsafeCell учитывается оптимизатором и означает, что неизменяемость внутреннего T нельзя предполагать только из факта существования &UnsafeCell<T>.
Минимальный однопоточный пример:
В примере set принимает &self, но меняет внутреннее значение. Это корректно для однопоточного использования, потому что API не выдаёт пользователю ссылку на внутренний T, а сам Slot не становится автоматически безопасным для совместного доступа из нескольких потоков.
UnsafeCell не разрешает произвольно создавать &mut T. Для такой ссылки по-прежнему требуется доказать уникальность изменяемого доступа на весь срок её жизни. Обычно безопасные абстракции избегают выдачи таких ссылок напрямую либо используют дополнительные правила: Cell ограничивает операции копируемыми значениями, RefCell проверяет заимствования во время выполнения, а Mutex сериализует доступ между потоками.
Особенно важно, что UnsafeCell не разрешает гонки данных. Если два потока одновременно изменяют один объект без синхронизации, наличие UnsafeCell не делает это допустимым. Для многопоточного доступа нужны подходящие атомарные операции, блокировка или другая доказуемая схема синхронизации.
Допустим, библиотеке нужен однопоточный объект с общим API чтения и изменения состояния. Вариант с обычным &mut self прост и полностью проверяется компилятором, но не подходит, если клиент должен хранить несколько общих ссылок на объект. Вариант с прямым raw pointer гибок, однако не даёт компилятору информации о внутренней изменяемости и оставляет все инварианты на авторе.
Можно применить UnsafeCell напрямую, как в примере. Это минимальная по накладным расходам основа, но автор должен вручную доказать отсутствие конфликтующих доступов и аккуратно ограничить API.
Для однопоточного кода обычно выбирают RefCell: он также использует внутреннюю изменяемость, но проверяет правила заимствования во время выполнения и вызывает панику при нарушении. Цена этого решения — дополнительное состояние и runtime-проверки. Если состояние разделяется между потоками, вместо него выбирают Mutex или другой синхронизирующий примитив.
Практически обоснованное решение — использовать UnsafeCell напрямую только при необходимости реализовать низкоуровневую абстракцию, а в прикладном коде предпочесть RefCell, Mutex, Cell или специализированный безопасный тип. Это уменьшает unsafe-код и переносит проверку инвариантов в уже проверенные компоненты.
UnsafeCell<T> тип безопасным для Sync?Нет. UnsafeCell разрешает внутреннюю изменяемость, но не добавляет синхронизацию и не предотвращает одновременный доступ из потоков. Если тип должен реализовывать Sync, его автор обязан отдельно доказать корректность совместного доступа; обычно для этого используются блокировки или атомарные операции.
&UnsafeCell<T> всегда получить безопасную &mut T?Нет. Метод get даёт raw pointer, а не безопасную изменяемую ссылку. Создание &mut T требует доказать отсутствие других активных доступов к тому же T, включая доступы, созданные ранее. UnsafeCell разрешает изменять внутреннее значение через общую ссылку, но не разрешает одновременно поддерживать несовместимые ссылки на это значение.
UnsafeCell принципиально отличается от хранения raw pointer?Raw pointer описывает адрес и не сообщает компилятору, что объект допускает изменение за общей ссылкой. UnsafeCell является специальным типом языка и фиксирует именно исключение из правила неизменяемости, сохраняя остальные требования к корректному доступу. Поэтому raw pointer может быть внутренним инструментом реализации, но сам по себе не заменяет UnsafeCell в абстракции внутренней изменяемости.