Сегодня мы поговорим про такую, казалось бы, простую и привычную штуку, как функция drop в языке Rust. Что может быть банальнее?
Создали переменную, и в конце области видимости она сама очистится. RAII за нас делает всю работу, а мы просто выдыхаем и не боимся утечек памяти. Но не все так радужно!
Иногда даже маленькая функция drop() способна подбросить неприятный сюрпризик и буквально поломать вашу программку, причём сделать это тихонько и незаметно. С чего бы вдруг такая базовая вещь приводит к ошибкам?
Как работает Drop в Rust
Rust не имеет сборщика мусора в привычном понимании. Память освобождается автоматически в тот момент, когда значение выходит из области видимости. За это отвечает механизм деструкторов, представленный типажом Drop. Когда мы говорим «вызывается drop», обычно имеем в виду как раз срабатывание деструктора объекта.
У большинства типов в Rust свой Drop не реализован, но деструктор по дефолту есть всегда, он рекурсивно вызывает drop для полей структуры и освобождает память под объект.
Мы не вызываем деструкторы вручную. Rust автоматом вызывает Drop::drop для объекта, когда тот больше не нужен. Стандартная библиотека вообще запрещает прямой вызов метода drop у типажа Drop. Если попытаться написать some_value.drop(), компилятор нам не даст это сделать. Вместо этого, если по каким‑то причинам нужно принудительно уничтожить объект до конца его обычного времени жизни, нам предлагают функцию std::mem::drop(some_value). Эта функция берёт переданное ей значение и немедленно его уничтожает, по сути, просто перемещает его в свою внутренность, после чего выход из функции drop вызывает нужный деструктор.
Рассмотрим простой сценарий.
Допустим, есть структура TempFile, которая при удалении должна удалить временный файл с диска. Когда переменная tmp типа TempFile выходит за пределы блока, вызывается drop(tmp), и файл удаляется автоматически. Мы даже не пишем ни строчки кода для этого, удобно...
struct TempFile {
path: PathBuf,
}
impl Drop for TempFile {
fn drop(&mut self) {
// попытка удалить файл
if let Err(e) = std::fs::remove_file(&self.path) {
eprintln!("Не удалось удалить временный файл: {}", e);
}
}
}
fn create_temp_file() -> TempFile {
let path = std::env::temp_dir().join("temp.txt");
std::fs::write(&path, b"hello").unwrap();
TempFile { path }
}
fn main() {
let tmp = create_temp_file();
// ... тут работаем с временным файлом ...
// в конце функции tmp будет автоматически удалён, файл очистится
}
impl Drop for TempFile определяет поведение при удалении: когда объект TempFile уничтожается, мы пытаемся удалить файл по указанному пути. В main мы просто создаём временный файл и ничего специально не делаем для его удаления. Деструктор сработает автоматически при выходе из main.
Всё выглядит прекрасно, пока мы говорим об автоматическом освобождении памяти.
Но стоит нам захотеть вручную контролировать момент очистки или просто погрузиться поглубже в механизм Drop — начинаются нюансики.
Когда нужно вызывать drop вручную?
В большинстве случаев не нужно вручную вызывать drop.
Rust и так всё делает правильно: очистит память, закроет файлы, отпустит мьютексы. Но бывают ситуации, когда момент освобождения ресурса влияет на логику. Например, вы пишете сервер, который принимает соединения, и хотели бы закрывать соединение (освобождать сокет) не тогда, когда объект Connection просто доживёт до конца функции, а пораньше. Скажем, сразу после обработки запроса. Можно, конечно, обернуть всё тело обработки в дополнительный блок { ... }, чтобы соединение вышло из области видимости раньше. Но это не всегда удобно и ухудшает читаемость кода. Проще явно вызвать drop(connection) в нужном месте, показав намерение: «всё, с соединением можно расстаться, освободи ресурсы сейчас».
Или другой сценарий. Представьте, у вас есть большая структура, занимающая мегабайты в памяти, и она больше не нужна, а функция ещё продолжается и конец её не близко. Держать мегабайты лишней памяти всё это время не хочется. Опять же, решением может стать явный вызов drop(heavy_struct) там, где вы эту структуру отработали и готовы освободить память под неё.
Короче говоря, вызов std::mem::drop полезен именно для управления временем жизни объекта вручную. Однако стоит помнить, что после drop(x) переменную x трогать уже нельзя, она больше не существует. Rust не позволит вам даже просто обратиться к ней, компилятор выдаст ошибку. Здесь, кстати, и скрывается одна из «ловушек»: если вы по ошибке попробуете использовать переменную после drop, программа просто не скомпилируется. Это не ломает рабочую программку как таковую, но может сломать ваш код на этапе компиляции, что, впрочем, лучше, чем ловить баги уже на самом проде.
Неожиданный конфликт
Рассмотрим теперь менее очевидный сценарий, где drop вмешивается в жизнь переменных так, что код перестаёт компилироваться. Звучит странно. Неужели простое добавление вызова drop способно вызвать ошибку компиляции там, где до этого всё работало? А такое бывает.
Дело в том, что у Rust есть умная фича: нелексические времена жизни. Это значит, что компилятор старается освободить переменные, точнее, завершить их время жизни как можно раньше, не дожидаясь конца блока, если видит, что они больше не используются. Например, так:
# fn main() {
let mut s = String::from("example");
println!("{}", s);
// переменная s больше не используется после печати.
// компилятор мог бы освободить память под s прямо здесь.
# }
В старые времена переменная s формально жила бы до конца блока main, хоть после println! она уже не нужна. С NLL компилятор понимает, что s можно прекратить использовать сразу после печати, и фактически освобождает ресурс раньше.
Однако работает это только для типов, у которых нет явного деструктора. Если мы реализуем для типа Drop, то вступают в силу другие правила. При наличии кастомного Drop компилятор обязан строго дождаться конца области видимости, чтобы вызвать ваш деструктор. Ресурс не будет освобождён заранее, даже если больше не используется. Почему так? Потому что вы могли прописать в Drop любой произвольный код, и Rust не рискует вызвать ваш деструктор раньше времени или пропустить его вызов вообще.
Предположим, есть структура Foo с ссылкой внутри. Напишем для неё Drop, пусть даже пустой и попробуем воспользоваться этой структурой:
struct Foo<'a, T>(&'a mut T);
impl<'a, T> Drop for Foo<'a, T> {
fn drop(&mut self) {
println!("Drop для Foo отработал");
}
}
fn main() {
let mut data = 42;
let foo = Foo(&mut data);
println!("Наше число: {}", data);
}
На первый взгляд, ничего особенного, создали Foo с mut‑ссылкой на data, потом пытаемся вывести data на экран. Казалось бы, ссылка foo больше не используется к моменту println! и могла бы быть удалена до вывода. Но при компиляции получим ошибку:
error[E0505]: cannot move out of `data` because it is borrowed
--> src/main.rs:10:28
|
8 | let foo = Foo(&mut data);
| ---------- borrow of `data` occurs here
9 | println!("Наше число: {}", data);
| ---- borrow later used here
10 | }
| ^ `data` moved out here while still borrowed
Произошло то, что Foo содержит ссылку &mut data и, раз Foo имеет свой Drop, компилятор не будет втихаря удалять foo до конца main. Переменная foo живёт до конца области видимости, потому что у неё есть важная миссия: вызвать Drop::drop. А раз foo живёт до конца блока, то и заимствование &mut data, хранящееся внутри foo, считается активным весь этот срок. Поэтому когда мы в println! пытаемся использовать data, компилятор видит, что data всё ещё занята (ведь foo не отпустил её). Отсюда и ошибка, data по‑прежнему заимствована, и трогать её нельзя.
Интересно, что стоит убрать реализацию Drop для Foo и код скомпилируется. А если не связывать Foo с именем, а сразу создать и тут же отбросить, всё будет хорошо:
# struct Foo<'a, T>(&'a mut T);
# impl<'a, T> Drop for Foo<'a, T> {
# fn drop(&mut self) { println!("drop Foo"); }
# }
# fn main() {
// создаём Foo, но не присваиваем его переменной, используем _
Foo(&mut data);
// Foo сразу же уничтожается, освобождая заимствование data
println!("Наше число: {}", data); // теперь работает
# }
Это работает потому что Foo(&mut data) в этом случае считается временным значением, которому мы не присваиваем имя. Rust видит, что результат явно игнорируется, и сразу же вызывает деструктор для этого временного значения. mut‑ссылка освобождается мгновенно, и мы снова можем пользоваться data. Разумеется, такой код не делает вообще ничего полезного.
Для практических нужд более пригоден другой подход: явный вызов drop(foo). Если мы понимаем, что дальше foo не нужен и хотим освободить data, можно вручную дропнуть foo перед использованием data:
# struct Foo<'a, T>(&'a mut T);
# impl<'a, T> Drop for Foo<'a, T> {
# fn drop(&mut self) { println!("drop Foo"); }
# }
# fn main() {
let mut data = String::from("example");
let foo = Foo(&mut data);
// ... работаем с foo ...
drop(foo); // вручную уничтожаем foo раньше времени
// теперь mut-ссылка освобождена, data снова доступна
println!("Строка после drop: {}", data);
# }
Мы сами решили, когда больше не нуждаемся в foo, и сразу освободили заимствование. С точки зрения компилятора, после drop(foo) переменная foo уже не существует, а значит &mut data больше не занята.
Итого, реализация Drop может продлить время жизни объекта до конца области видимости, даже если он больше не используется. Это способно вызвать ошибки там, где код работает с заимствованиями. Лекарство в том, чтобы просто вызвать drop явно в нужный момент, либо не реализовывать Drop, если без него можно обойтись.
Частичное перемещение и защита от Drop
Когда вы реализуете Drop для структуры, вы тем самым накладываете некоторые ограничения на использование этой структуры. Например, нельзя вынимать поля из такой структуры обычным способом, потому что компилятору нужно гарантировать вызов деструктора на весь объект целиком.
Представьте структуру:
struct Person {
name: String,
age: u8,
}
impl Drop for Person {
fn drop(&mut self) {
println!("Прощай, {}!", self.name);
}
}
Простой Person с именем и возрастом, и в деструкторе мы просто печатаем прощальное сообщение. Казалось бы, всё отлично, но реализация Drop означает, что следующий код уже не скомпилируется:
# struct Person { name: String, age: u8 }
# impl Drop for Person {
# fn drop(&mut self) {
# println!("Прощай, {}!", self.name);
# }
# }
# fn main() {
let p = Person { name: "Kolyan".into(), age: 30 };
// Попытка вынуть поле name
let name = p.name;
# }
При компиляции мы увидим ошибку от проверяющего заимствования (borrow checker):
error[E0509]: cannot move out of type `Person`, which implements the `Drop` trait
--> src/main.rs:6:18
|
4 | let p = Person { name: String::from("Kolyan"), age: 30 };
| - переменная `p` создана здесь
5 | // Попытка вынуть поле name
6 | let name = p.name;
| ^ перемещение поля запрещено, так как `Person` реализует Drop
Rust отказывает нам в праве забрать из структуры поле name, потому что тогда при удалении p деструктор Drop::drop не сможет корректно выполнить свою работу, поля уже нет, мы его украли. Ведь в Drop мы планировали попрощаться с человеком, используя его имя, а имени уже нет, мы забрали его ранее. Чтобы не допустить такой ситуации, компилятор запрещает вынимать значения из структур, которые реализуют Drop (а еще это называют Drop Check).
Как же быть, если всё‑таки очень нужно вынуть поле? Вариант: использовать функции вроде std::mem::replace или std::mem::take, которые позволяют заменить значение в поле на какое‑то пустое, а старое значение вернуть. В нашем примере, чтобы достать name до удаления Person, можно сделать так:
# use std::mem;
# struct Person { name: String, age: u8 }
# impl Drop for Person {
# fn drop(&mut self) {
# println!("Прощай, {}!", self.name);
# }
# }
# fn main() {
let mut p = Person { name: "Kolyan".into(), age: 30 };
let name = mem::replace(&mut p.name, String::new());
// теперь в p.name пустая строка, а настоящее имя мы забрали
println!("Имя извлечено: {}", name);
# }
Аккуратно вынули имя, подменив его на пустую строку. Когда p будет удаляться, деструктор увидит пустое имя. Что ж, попрощается без него. По крайней мере мы избежали запрета компилятора. Такое действие годится, если данные действительно нужно изъять. Если же цель была просто воспользоваться полем, но не удалять его из структуры насовсем, можно вместо перемещения взять ссылку &p.name или скопировать значение, если оно реализует Copy.
Общий принцип: реализуя Drop, вы несколько ограничиваете возможности по перемещению значений внутри структуры. Компилятор будет требовать, чтобы ваш объект дожил целиком до вызова drop. Если это проблематично, придётся искать обходные пути (как показано выше с mem::replace) или пересмотреть дизайн, возможно, типу Drop вовсе не нужен.
Рекурсивный Drop и опасность переполнения стека
Ещё одна интересная проблемка связана с тем, как именно Rust освобождает сложные структуры данных.
Представьте себе связный список из миллионов элементов. Если на самый верхний элемент списка вызвать drop, что произойдёт? Вызовется деструктор, который в свою очередь вызовет drop на следующем элементе, и так далее. Это рекурсивный спуск, и при списке в миллион элементов есть риск получить переполнение стека.
По дефолту Rust генерирует код деструктора для структуры рекурсивно. Сначала вызывается drop для полей, потом выполняется ваш код в Drop. В случае списка, где каждый узел содержит Option<Box<Node>> или подобную конструкцию, удаление миллиона вложенных Box превратится в цепочку вызовов глубиной в миллион.
В большинстве случаев эта проблема решается оптимизацией компилятора. Tail‑call optimization и похожие техники умеют свернуть последовательные вызовы drop в цикл. Однако если вы реализуете Drop самостоятельно и делаете в нём что‑то нетривиальное, есть шанс нарушить эти оптимизации. В итоге при уничтожении очень большой структуры вы можете столкнуться с падением программы из‑за переполнения стека.
Что можно сделать, чтобы себя обезопасить:
Проектировать структуры и их
Dropтак, чтобы избегать глубокого рекурсивного удаления. К пример просто освобождать длинные списки в явном цикле вместо рекурсии.Иногда имеет смысл использовать контейнер
Vecили похожий, которые освобождают своё содержимое циклом внутри, а не по одному элементу рекурсивно.Возможно стоит написать специальную логику очистки для каких-то важных мест. Например, вручную обходить структуру и вызывать
dropдля элементов в цикле, если дефолтный способ может привести к проблемам.
Честно говоря, в повседневной практике с таким лично я сталквался очень редко. Но помнить об этой возможности стоит.
Ловушки Drop в небезопасном коде
До сих пор мы обсуждали безопасный Rust. Однако Drop может преподнести сюрпризы и при работе с unsafe. Проблема в том, что некоторые операции неявно вызывают деструкторы, и если вы не учли этого, можно получить неопределённое поведение или крах программы.
Рассмотрим пример:
use std::ptr;
unsafe fn double_free_demo() {
let mut num = Box::new(10);
let num_ptr: *mut i32 = &mut *num;
// Явно уничтожаем объект, на который указывает указатель
ptr::drop_in_place(num_ptr);
// Теперь num "висячий": память уже освобождена
// Попытка освободить её повторно при выходе из функции вызовет double free
}
Здесь выделили Box::new(10). На куче лежит число 10, а num владеет этим числом. Затем мы получили сырой указатель num_ptr на содержимое. Функция ptr::drop_in_place выполняет деструктор по указанному адресу, по сути освобождая память, на которую указывает num_ptr. После её вызова ресурс уже освобождён, но переменная num по‑прежнему существует и при выходе из области видимости снова попробует освободить тот же самый адрес. Это классическая ситуация double free.
Почему компилятор этого не предотвратил? Потому что мы были в блоке unsafe и сами заявили: «всё проконтролирую, мне можно и я крутой». В безопасном Rust такой трюк вообще невозможен, нельзя вызвать деструктор вручную без перемещения значения.
Чтобы исправить код, нам следовало сразу после ptr::drop_in_place(num_ptr) что‑то сделать с num. Например, присвоить num новый Box или пометить переменную как более не действительную. Существует функция std::mem::forget, которая полностью отключает вызов деструктора. В данном случае, чтобы избежать double free, можно было вызвать mem::forget(num) сразу после drop_in_place. Это лишило бы num права освобождать память (Rust решил бы, что ресурс уже нам не принадлежит), и второй раз память не тронулась бы. Правда, утечка памяти осталась бы на нашей совести.
Другой подвох с Drop в unsafe связан с присваиванием через указатель. Например:
use std::ptr;
unsafe fn overwrite_demo() {
let mut x = Box::new(String::from("Hello"));
let ptr_to_x: *mut Box<String> = &mut x;
// cоздаём новую коробку со строкой
let new_value = Box::new(String::from("New"));
// gереписываем по указателю новое значение
ptr::write(ptr_to_x, new_value);
// в этот момент старый Box<String> в x был заменён и не очищен
println!("x = {}", x);
}
Используем ptr::write для записи нового значения в память, на которую указывает ptr_to_x. В отличие от обычного присваивания *ptr_to_x = new_value, эта функция не вызывает drop для старого значения, которое там раньше находилось.
Если бы мы сделали обычное присваивание через разыменование указателя, Rust попытался бы сначала удалить старое значение в x, а потом записать новое. Но так как мы используем специальную функцию ptr::write, старый Box просто замещается новым без вызова деструктора. В данном случае это даже супер, потому что переменная x всё ещё владеет своим старым значением, и его деструктор вызовется автоматически при выходе из функции. Нам же важно было избежать двойного освобождения или ситуации, когда пытались бы очистить неинициализированную память.
Поэтому как всегда в unsafe‑коде будьте осторожны с операциями, которые могут вызывать (или пропускать) деструкторы неявно.
В итоге
drop и вообще весь механизм деструкторов является одним из самых важных элементов системы безопасности Rust, позволяющий автоматически освобождать ресурсы и избегать утечек памяти. Но, как мы выяснили, даже у такого простого механизма есть нюансы.
Кратко вспомним основные «поломки», которые может привнести drop:
Реализация
Dropдля типа мешает компилятору освободить этот тип раньше времени.Тип с
Dropнельзя частично перемещать (вынимать поля), иначе деструктор останется без нужных данных.Рекурсивные деструкторы могут вызвать переполнение стека, если структура очень глубока.
В небезопасном коде важно помнить про деструкторы: вручную вызывая
dropчерез указатели или перезаписывая данные, легко получить неопределённое поведение.Никогда не вызывайте метод
Drop::dropнапрямую. Вызывайте его только черезstd::mem::drop.
В 99% случаев вам не нужно бояться drop. Rust намеренно сделан таким, чтобы вы просто наслаждались автоматическим управлением ресурсами.
Успешной отладки, и пусть ваши программы никогда не падают в рантайме, ну а если и таки падают, то только контролируемо через panic!, и уж точно не из‑за коварного drop.
С Новым Годом!
Размещайте облачную инфраструктуру и масштабируйте сервисы с надежным облачным провайдером Beget.
Эксклюзивно для читателей Хабра мы даем бонус 10% при первом пополнении.

Комментарии (6)

Dotarev
07.01.2026 08:32# struct Person { name: String, age: u8 }...
Как же быть, если всё‑таки очень нужно вынуть поле? Вариант: использовать функции вродеstd::mem::replaceилиstd::mem::take, которые позволяют заменить значение в поле на какое‑то пустое, а старое значение вернуть.Тут лучше другой вариант: подумать ещё пять раз, а так ли нужно изменять экземпляр Person, и не лучше ли настучать по рукам тому, кто объявляет структуру изменяемой:
let mut p = Person {...}После трюка
let name = mem::replace(&mutp.name, String::new());уже нельзя использовать саму структуру, но узнаете это вы только после того, как всё же сделаете это через триста строк и потратите кучу времени на вылавливание бага, который проявляется только в рантайме.

Jijiki
07.01.2026 08:32я не експерт, просто где-то видел и решил показать, как наверно обходят дроп, в Расте мы же можем говорить компилятору чо делать)
Скрытый текст
pub struct Person { x: String, y: u8 } impl Person { pub fn remove(P: &mut Person) -> Option<Person>{ Some(Person{x:"".to_string(),y:0}) } } fn main() { let mut p = Person { x: "q".into(), y: 0 }; println!("{}",p.x); if let Some(mut qqq) = Person::remove(&mut p){ println!("{}",qqq.x); }; }

Boneyan
07.01.2026 08:32По дефолту Rust генерирует код деструктора для структуры рекурсивно. Сначала вызывается drop для полей, потом выполняется ваш код в Drop.
Вроде как наоборот - сначала вызывается наш Drop, потом drop для полей. Но м.б. вы что-то другое имели в виду.
Но в целом статья хорошая.

MaNaXname
07.01.2026 08:32LLMка еще может путаться/галюцинировать. Я вон в другой статье прочел что если вернуть значение из функции как ссылку ( со *) то компилятор разместит значение в куче.

vabka
07.01.2026 08:32Ждал упоминание std::mem::ManuallyDrop, но не увидел, хотя штука тоже может больно отстрелить.
https://doc.rust-lang.org/std/mem/struct.ManuallyDrop.html
Mingun
Хорошая статься, но есть пару вводящих в заблуждение моментов:
Какая утечка памяти? Мы же уже выяснили, что
ptr::drop_in_placeосвободил ее, аmem::forgetсказал компилятору, что там освобождено и предотвратил double free.Ничего не супер, в
xтеперь же "New", а для "Old"dropтак и не вызвался (new_valueперемещен внутрь ptr::write, который его затем закидывает вx, поэтому для негоdropне вызывается)