сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Владение и лайфтаймы

Drop и RAII в Rust

Drop и RAII

Тут проверяют понимание жизненного цикла ресурсов: когда вызывается деструктор, в каком порядке, можно ли его позвать руками и гарантирован ли он вообще. Вопрос практический - от него зависит, закроется ли файл и снимется ли блокировка на ветке с ошибкой.

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

// Формулировки: «когда вызовется drop?», «почему d.drop() не компилируется?», «гарантирован ли Drop?»

Ресурс живёт, пока живёт владелец

RAII (resource acquisition is initialization) в Rust получается сам собой из владения: значение уничтожается, когда владелец выходит из области видимости, когда переменной присваивают новое значение или когда владение уехало и умерло уже там. Компилятор вставляет вызовы drop сам, и моменты освобождения известны на этапе компиляции - без пауз сборщика мусора.

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

let _a = D("a");
let _b = D("b");
// печатает: drop b, затем drop a
RAII
освобождение ресурса привязано ко времени жизни владельца
порядок drop
локальные - обратный объявлению, поля - прямой

Почему drop нельзя позвать руками

Метод Drop::drop принимает &mut self и не потребляет значение. Если бы его разрешили звать напрямую, деструктор отработал бы дважды: один раз вручную, второй - при выходе из области видимости. Поэтому такой вызов запрещён, ошибка E0040.

Уронить значение раньше времени можно через std::mem::drop(d): эта функция принимает значение по владению, и деструктор срабатывает в конце её пустого тела. Приём часто используют, чтобы освободить блокировку до конца функции - хотя чаще для этого хватает вложенного блока.

// Ещё одна деталь, о которой спрашивают: ранний выход через ? - обычный возврат, деструкторы отрабатывают. Именно поэтому в Rust нет блока finally.

E0040
явный вызов деструктора запрещён
mem::drop
функция, забирающая значение и роняющая его на месте

Гарантия не абсолютна

Запуск деструктора - не обещание языка, а следствие владения. Есть законные способы его пропустить: std::mem::forget поглощает значение без вызова drop, Box::leak превращает значение в &'static, цикл Rc не даёт счётчику обнулиться, а profile с panic = abort обрывает процесс без разворачивания стека.

Заметьте: всё это безопасно с точки зрения языка. Утечка памяти не портит память и не нарушает типовые гарантии, поэтому mem::forget обычная функция, а не unsafe.

// Практический вывод для прода: если потеря ресурса недопустима - например, надо гарантированно закоммитить транзакцию, не полагайтесь только на Drop, оставляйте явный commit или close.

mem::forget
поглощение значения без вызова деструктора
panic = abort
режим, где паника обрывает процесс без Drop

Как отвечать: «Гарантирует ли Rust вызов Drop?»

При обычном выходе из области видимости - да, компилятор вставляет вызов сам, и это работает в том числе на ранних выходах через ? и при разворачивании стека после паники. Но абсолютной гарантии нет: std::mem::forget поглощает значение без деструктора, Box::leak сознательно отдаёт память навсегда, цикл из Rc не даёт счётчику обнулиться, а сборка с panic = abort обрывает процесс без разворачивания. Всё это безопасно с точки зрения языка - утечка не портит память. Поэтому для критичных вещей вроде коммита транзакции я не полагаюсь только на Drop, а оставляю явный вызов.

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

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

  • Уверенно говорят, что Drop вызывается всегда. Утечка законна, а abort пропускает деструкторы.
  • Пробуют вызвать d.drop() вместо std::mem::drop(d).
  • Забывают, что локальные переменные уничтожаются в обратном порядке, а для guard'ов это критично.
  • Не знают, что при раннем выходе через ? деструкторы отрабатывают, и ищут аналог finally.
  • Считают mem::forget небезопасной функцией - она безопасна, потому что утечка не портит память.

Проверьте себя

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

  1. #rs_drop_raii1 / 5
    Почему вызов d.drop() не компилируется, а drop(d) — работает?
    A)d.drop() требует &mut d, а у d нет модификатора mut
    B)drop реализован только для типов из стандартной библиотеки
    C)Явный вызов деструктора запрещён; drop(d) съедает значение
    D)Разницы нет: обе формы вызывают один и тот же метод
    показать ответ и разбор
    +C)Явный вызов деструктора запрещён; drop(d) съедает значение

    // разбор: Метод Drop::drop берёт &mut self и не потребляет значение — если бы его разрешили звать руками, деструктор отработал бы дважды. Отсюда E0040. Функция std::mem::drop устроена иначе: она принимает значение по владению и уничтожает его вместе с концом своего тела — всё тело у неё пустое.

  2. #rs_drop_raii2 / 5
    Что делает std::mem::forget(v) и почему эта функция безопасна (не unsafe)?
    A)Освобождает память, не вызывая пользовательский Drop
    B)Помечает значение как перемещённое, чтобы обойти borrow checker
    C)Откладывает вызов деструктора до конца программы
    D)Забывает значение без вызова деструктора: это утечка
    показать ответ и разбор
    +D)Забывает значение без вызова деструктора: это утечка

    // разбор: forget поглощает значение и не запускает drop: буфер остаётся невозвращённым, ресурс — незакрытым. Это утечка, но не порча памяти, а безопасность в Rust про второе — поэтому функция обычная, без unsafe. Тот же эффект даёт зацикленный Rc или Box::leak, который сознательно превращает значение в &'static.

  3. #rs_drop_raii3 / 5
    Соединение с базой закрывается в Drop. Гарантирует ли Rust, что деструктор отработает?
    A)Да: рантайм вызывает деструктор при завершении программы
    B)Обычно да, но есть исключения: forget, цикл Rc, abort
    C)Нет: порядок и факт вызова зависят от оптимизаций компилятора
    D)Да, если тип не реализует Copy и не лежит в статической памяти
    показать ответ и разбор
    +B)Обычно да, но есть исключения: forget, цикл Rc, abort

    // разбор: Запуск деструкторов — не обещание языка, а следствие владения: при обычном выходе из области видимости он гарантирован, но mem::forget, цикл Rc, Box::leak или panic = abort его пропустят. Отсюда правило для критичных ресурсов: не полагаться только на Drop, а иметь явный close или commit там, где потеря недопустима.

  4. #rs_drop_raii4 / 5
    Структура хранит String и Vec. Нужно ли писать Drop, чтобы освободить их память?
    A)Да, иначе буферы полей утекут: освобождать их некому
    B)Нет: поля уничтожаются сами, рекурсивно по составу типа
    C)Да, но только для полей в куче
    D)Нет, если структура помечена #[derive(Drop)]
    показать ответ и разбор
    +B)Нет: поля уничтожаются сами, рекурсивно по составу типа

    // разбор: Компилятор генерирует уничтожение по составу: сначала для самого типа, если у него есть Drop, затем для каждого поля в порядке объявления, и так вглубь. Свой Drop пишут не ради памяти, а ради внешних эффектов — закрыть соединение, снять блокировку, записать метрику. Трейта Drop в derive нет: его реализуют руками.

  5. #rs_drop_raii5 / 5
    У структуры есть свой Drop и два поля с деструкторами. В каком порядке всё уничтожается?
    A)Сначала Drop структуры, затем поля в порядке объявления
    B)Сначала поля в порядке объявления, затем Drop структуры
    C)Сначала поля в обратном порядке объявления, затем тело Drop структуры
    D)Порядок не определён и зависит от оптимизаций
    показать ответ и разбор
    +A)Сначала Drop структуры, затем поля в порядке объявления

    // разбор: Сначала выполняется тело вашего Drop — иначе оно работало бы с уже уничтоженными полями, — и только потом уничтожаются поля, сверху вниз по объявлению. Это отличается от локальных переменных в блоке: те уничтожаются в обратном порядке объявления. Знание порядка нужно, когда поля связаны: например, буфер надо сбросить в файл до того, как файл закроется.

дальше

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

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