Drop и RAII в Rust
Тут проверяют понимание жизненного цикла ресурсов: когда вызывается деструктор, в каком порядке, можно ли его позвать руками и гарантирован ли он вообще. Вопрос практический - от него зависит, закроется ли файл и снимется ли блокировка на ветке с ошибкой.
Стержень: освобождение привязано к владению, порядок уничтожения обратный объявлению, а сама гарантия запуска деструктора не абсолютна.
// Формулировки: «когда вызовется 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, остальные разбираются в тренажёре.
- Почему вызов d.drop() не компилируется, а drop(d) — работает?A)d.drop() требует &mut d, а у d нет модификатора mutB)drop реализован только для типов из стандартной библиотекиC)Явный вызов деструктора запрещён; drop(d) съедает значениеD)Разницы нет: обе формы вызывают один и тот же метод
показать ответ и разбор
+C)Явный вызов деструктора запрещён; drop(d) съедает значение// разбор: Метод Drop::drop берёт &mut self и не потребляет значение — если бы его разрешили звать руками, деструктор отработал бы дважды. Отсюда E0040. Функция std::mem::drop устроена иначе: она принимает значение по владению и уничтожает его вместе с концом своего тела — всё тело у неё пустое.
- Что делает std::mem::forget(v) и почему эта функция безопасна (не unsafe)?A)Освобождает память, не вызывая пользовательский DropB)Помечает значение как перемещённое, чтобы обойти borrow checkerC)Откладывает вызов деструктора до конца программыD)Забывает значение без вызова деструктора: это утечка
показать ответ и разбор
+D)Забывает значение без вызова деструктора: это утечка// разбор: forget поглощает значение и не запускает drop: буфер остаётся невозвращённым, ресурс — незакрытым. Это утечка, но не порча памяти, а безопасность в Rust про второе — поэтому функция обычная, без unsafe. Тот же эффект даёт зацикленный Rc или Box::leak, который сознательно превращает значение в &'static.
- Соединение с базой закрывается в Drop. Гарантирует ли Rust, что деструктор отработает?A)Да: рантайм вызывает деструктор при завершении программыB)Обычно да, но есть исключения: forget, цикл Rc, abortC)Нет: порядок и факт вызова зависят от оптимизаций компилятораD)Да, если тип не реализует Copy и не лежит в статической памяти
показать ответ и разбор
+B)Обычно да, но есть исключения: forget, цикл Rc, abort// разбор: Запуск деструкторов — не обещание языка, а следствие владения: при обычном выходе из области видимости он гарантирован, но mem::forget, цикл Rc, Box::leak или panic = abort его пропустят. Отсюда правило для критичных ресурсов: не полагаться только на Drop, а иметь явный close или commit там, где потеря недопустима.
- Структура хранит String и Vec. Нужно ли писать Drop, чтобы освободить их память?A)Да, иначе буферы полей утекут: освобождать их некомуB)Нет: поля уничтожаются сами, рекурсивно по составу типаC)Да, но только для полей в кучеD)Нет, если структура помечена #[derive(Drop)]
показать ответ и разбор
+B)Нет: поля уничтожаются сами, рекурсивно по составу типа// разбор: Компилятор генерирует уничтожение по составу: сначала для самого типа, если у него есть Drop, затем для каждого поля в порядке объявления, и так вглубь. Свой Drop пишут не ради памяти, а ради внешних эффектов — закрыть соединение, снять блокировку, записать метрику. Трейта Drop в derive нет: его реализуют руками.
- У структуры есть свой Drop и два поля с деструкторами. В каком порядке всё уничтожается?A)Сначала Drop структуры, затем поля в порядке объявленияB)Сначала поля в порядке объявления, затем Drop структурыC)Сначала поля в обратном порядке объявления, затем тело Drop структурыD)Порядок не определён и зависит от оптимизаций
показать ответ и разбор
+A)Сначала Drop структуры, затем поля в порядке объявления// разбор: Сначала выполняется тело вашего Drop — иначе оно работало бы с уже уничтоженными полями, — и только потом уничтожаются поля, сверху вниз по объявлению. Это отличается от локальных переменных в блоке: те уничтожаются в обратном порядке объявления. Знание порядка нужно, когда поля связаны: например, буфер надо сбросить в файл до того, как файл закроется.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.