← вся теориятеория к собесу

unsafe в Rust

unsafe — не выключатель безопасности. Он не отменяет ни проверку типов, ни владение, ни заимствование, а разрешает пять конкретных операций, за корректность которых теперь отвечаешь ты, а не компилятор. Ответ «там отключаются проверки» на собесе сразу роняет грейд.

разбор писал Даниил, автор Сеньорчика

Что такое unsafe

Про unsafe спрашивают, чтобы проверить, не считаешь ли ты его выключателем безопасности. Ответ «там отключаются проверки» сразу снижает грейд. Реальный вопрос - понимаешь ли ты, какие именно гарантии язык перекладывает на тебя.

Стержень: unsafe разрешает пять конкретных операций и не отменяет ни проверку типов, ни владение, ни заимствование.

// Формулировки: «что разрешает unsafe?», «чем unsafe fn отличается от блока?», «что такое UB?»

Закрытый список из пяти вещей

unsafe разрешает: разыменовать сырой указатель, вызвать unsafe-функцию или функцию из FFI (foreign function interface), обратиться к изменяемому static, прочитать поле union, реализовать unsafe-трейт. Всё. Проверка типов, правила владения и заимствования внутри блока работают ровно как снаружи.

Отсюда правило гигиены: блок делают коротким. Чем меньше кода внутри, тем точнее видно, за что именно автор взял ответственность, и тем легче это ревьюить. Здоровая практика - писать над каждым unsafe комментарий SAFETY с обоснованием.

// Классическая формулировка: unsafe не выключает проверки, он даёт доступ к операциям, корректность которых компилятор проверить не в состоянии.

unsafe
доступ к пяти непроверяемым операциям
SAFETY-комментарий
обоснование, почему конкретный unsafe корректен

Безопасная абстракция и unsafe fn

Половина стандартной библиотеки построена на unsafe: Vec, String, split_at_mut внутри работают с сырыми указателями. Но снаружи их API безопасно, потому что автор доказал инварианты - длина не превышает ёмкость, срезы не пересекаются. Так цена доверия сосредоточена в маленьком проверяемом модуле.

Отличие unsafe fn от unsafe-блока именно в том, кто отвечает. Помеченная функция говорит: «вызывать можно только при таких условиях, проверить их обязан вызывающий», поэтому у неё в документации есть раздел Safety. Блок внутри обычной функции говорит обратное: автор всё проверил, наружу выдаётся безопасный интерфейс.

// Ни та, ни другая форма не влияет на сгенерированный код: проверки границ в рантайме остаются на месте.

safe abstraction
безопасное API поверх внутреннего unsafe
unsafe fn
функция, условия вызова которой проверяет вызывающий

UB и как его ловят

Неопределённое поведение это не «программа упала». Это ситуация, о которой компилятор имел право предполагать, что её не бывает: висячая ссылка, две живые &mut на одно место, bool с байтом 2, гонка данных. Дальше оптимизатор пользуется этим предположением и вправе выбросить или переставить код.

Опаснее всего то, что симптомов может не быть годами, а сломается всё от смены версии компилятора или уровня оптимизации. Паника и выход за границу массива к UB (undefined behavior) не относятся это определённое поведение с внятной диагностикой.

// Ловят UB интерпретатором Miri: он выполняет код по правилам абстрактной машины Rust и замечает чтение неинициализированной памяти, выход за границы выделения, нарушение правил алиасинга. Работает медленно и не покрывает FFI, поэтому его гоняют на юнит-тестах вокруг unsafe.

UB
нарушение предположений, на которых строятся оптимизации
Miri
интерпретатор MIR (mid-level intermediate representation), ловящий нарушения модели памяти

Как отвечать: «Что даёт блок unsafe?»

Он даёт доступ к пяти операциям, корректность которых компилятор проверить не может: разыменование сырого указателя, вызов unsafe-функции или FFI, обращение к изменяемому static, чтение поля union и реализация unsafe-трейта. Всё остальное продолжает работать - типы, владение, заимствование внутри блока проверяются как обычно. То есть unsafe не выключает безопасность, а перекладывает конкретную проверку на меня. Поэтому блоки держу короткими и пишу над ними SAFETY-комментарий, а сам unsafe стараюсь прятать за безопасным API - как это сделано у Vec и split_at_mut.

Ты сразу опровергаешь миф про «отключение проверок», называешь конкретный список и добавляешь практику. Это ответ, после которого дальше спрашивают уже про UB, а не про основы.

На чём валятся

  • Говорят, что unsafe отключает borrow checker. Он продолжает работать.
  • Помечают unsafe целую функцию вместо короткого блока и размывают ответственность.
  • Путают UB с падением программы: UB может годами не давать симптомов.
  • Не знают про Miri и считают, что unsafe-код проверить нечем.
  • Не могут объяснить, почему Vec безопасен снаружи, хотя внутри полон unsafe.

Проверь себя

Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.

  1. #rs_unsafe1 / 5
    Что такое безопасная абстракция над unsafe?
    A)Код, где unsafe спрятан за макросом и не виден в исходниках
    B)Функция, помеченная unsafe, но не нарушающая правил языка
    C)Обёртка, которая ловит нарушения инвариантов в рантайме и паникует
    D)Публичное API без unsafe: инварианты держит автор
    показать ответ и разбор
    +D)Публичное API без unsafe: инварианты держит автор

    // разбор: Так устроена половина стандартной библиотеки: Vec, String и split_at_mut внутри работают с сырыми указателями, а снаружи безопасны, потому что автор доказал инварианты — длина не больше ёмкости, срезы не пересекаются. Пользователю unsafe писать не нужно, а цена доверия сосредоточена в одном небольшом модуле.

  2. #rs_unsafe2 / 5
    Чем unsafe fn отличается от unsafe-блока внутри обычной функции?
    A)unsafe fn перекладывает проверку условий на вызывающего
    B)unsafe-блок допускает больше операций, чем тело unsafe fn
    C)unsafe fn выполняется быстрее: компилятор снимает проверки границ
    D)Разницы нет: обе формы помечают код как непроверяемый
    показать ответ и разбор
    +A)unsafe fn перекладывает проверку условий на вызывающего

    // разбор: Пометка функции — это контракт: «вызывать можно лишь при таких-то условиях, проверить их обязан вызывающий». Именно поэтому у unsafe fn в документации есть раздел Safety. Блок внутри обычной функции говорит обратное: автор проверил всё сам, снаружи API безопасно. Проверки границ в рантайме ни та, ни другая форма не снимает.

  3. #rs_unsafe3 / 5
    Что считается неопределённым поведением (UB) в Rust?
    A)Ситуация, когда программа даёт разный результат на разных платформах
    B)Падение программы, включая панику и выход за границу массива
    C)Слом правил, на которые опирается компилятор: висячие ссылки, гонки
    D)Обращение к памяти, освобождённой сборщиком мусора
    показать ответ и разбор
    +C)Слом правил, на которые опирается компилятор: висячие ссылки, гонки

    // разбор: UB — это не «упало», а «компилятор имел право предполагать, что такого не бывает». Ссылка на освобождённую память, два &mut на одно значение, bool с байтом 2, гонка данных — после такого оптимизатор вправе выкинуть код или переставить его. Паника и выход за границу массива к UB не относятся: это определённое поведение с диагностикой.

  4. #rs_unsafe4 / 5
    Чем проверяют unsafe-код на скрытое UB?
    A)clippy с включённым набором правил pedantic
    B)Miri — интерпретатор MIR, ловит нарушения модели памяти
    C)Отладчик gdb с точками останова на обращениях к памяти
    D)Компилятор в режиме -O: оптимизатор сообщает о найденных нарушениях
    показать ответ и разбор
    +B)Miri — интерпретатор MIR, ловит нарушения модели памяти

    // разбор: Miri выполняет код по правилам абстрактной машины Rust и ловит то, что не видно в обычной сборке: чтение неинициализированной памяти, выход за пределы выделения, нарушение правил алиасинга. Работает медленно и не покрывает FFI, поэтому его гоняют на юнит-тестах вокруг unsafe. Clippy разбирает стиль, компилятор о UB не предупреждает.

  5. #rs_unsafe5 / 5
    Отключает ли unsafe проверку заимствований?
    A)Да, внутри блока правила заимствования не действуют и компилятор их пропускает
    B)Да, но только для сырых указателей, объявленных внутри этого же блока кода
    C)Нет: проверки остаются, блок лишь разрешает пять дополнительных операций
    D)Нет, но внутри блока предупреждения borrow checker понижаются до уровня подсказок
    показать ответ и разбор
    +C)Нет: проверки остаются, блок лишь разрешает пять дополнительных операций

    // разбор: Внутри unsafe работают все обычные правила: владение, заимствование, лайфтаймы, проверка типов. Блок добавляет ровно пять возможностей — разыменовать сырой указатель, вызвать unsafe-функцию, обратиться к изменяемому static, прочитать поле union, реализовать unsafe-трейт. Отсюда и распространённое заблуждение «unsafe отключает проверки»: он снимает конкретные запреты, а не защиту целиком.

дальше

Теорию прочитал. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.