Введение#
При работе с системными ресурсами - выделенной памятью в куче, открытыми файлами или сетевыми сокетами - одна из главных трудностей в программировании заключается в их своевременном и гарантированном освобождении. Забытый вызов free() ведет к постоянным утечкам памяти, а преждевременное удаление - к уязвимостям вроде use-after-free или double free.
В Rust за порядок с ресурсами отвечает идиома управления ресурсами RAII (Resource Acquisition Is Initialization).
В языках с ручным управлением разработчик вынужден лично отслеживать точки выхода из функций, а в языках со сборщиком мусора (Java, Go, Python) специальный рантайм фоном ищет неиспользуемые объекты. В Rust же компилятор автоматически связывает время жизни ресурса со временем жизни его владельца.
Сегодня мы разберем, как работает идиома RAII, для чего нужен владеющий указатель Box<T>, когда полезно отменить автоматическое освобождение через Box::leak и как устроено автоматическое приведение ссылок Deref Coercion.
1. Идиома RAII и трейт Drop#
В Rust владение ресурсом (память в куче, открытый файл, сетевой сокет, мютекс) обычно привязано к времени жизни значения.
- Ресурс захватывается при создании переменной (в ее конструкторе или функции
new). - Ресурс освобождается в тот момент, когда переменная выходит за пределы своей области видимости (
scope).
Как устроен трейт Drop?#
За очистку ресурсов отвечает трейт std::ops::Drop. Когда значение выходит из области видимости, Rust автоматически запускает его Drop::drop, если тип реализует Drop. После этого автоматически уничтожаются его поля.
struct CustomResource {
name: String,
}
impl Drop for CustomResource {
fn drop(&mut self) {
println!("Освобождаем ресурс: {}", self.name);
}
}
fn main() {
{
let res = CustomResource { name: "Файл конфигурации".into() };
println!("Работаем с ресурсом...");
} // <- Переменная res выходит из блока: автоматически вызывается drop()!
println!("Программа идет дальше.");
}Вы не можете вызвать метод res.drop() вручную напрямую - компилятор запретит это, чтобы избежать ошибки двойного освобождения (double free). Если вам нужно принудительно освободить ресурс раньше конца блока, используйте функцию std::mem::drop(res).
Всегда ли освобождение ресурса гарантировано?#
Трейт Drop не является абсолютной гарантией освобождения ресурса при любых обстоятельствах. Деструкторы не вызовутся, если:
std::mem::forget(res)намеренно предотвращает уничтожение значения и, соответственно, вызовDrop.- Циклическая структура из
RcилиArcможет сохранить значения живыми навсегда: счетчик ссылок не достигает нуля, поэтомуDropдля объектов в цикле не вызывается. - Происходит экстренное аварийное завершение процесса (
SIGKILL,std::process::abort).
2. Владеющий указатель Box#
Локальные значения в Rust обычно размещаются на стеке. Если нужно явно разместить значение в куче, используется Box<T>.
Зачем нужен Box?#
Сам Box<T> представляет собой владеющий указатель на объект в куче. Для обычного Sized-типа размер Box<T> на стеке обычно соответствует размеру указателя - 8 байт на 64-битной платформе.
Box<T> используется в нескольких ключевых сценариях:
- Рекурсивные типы данных: Без
Boxкомпилятор не сможет определить конечный размер типа в памяти:Здесьenum List { Cons(i32, Box<List>), Nil, }Boxнеобходим не ради оптимизации: без негоListсодержал бы сам себя напрямую, вызывая бесконечную рекурсию при расчете размера. - Размещение крупных структур в куче: Когда большой объект удобнее хранить отдельно от локальных данных на стеке.
- Хранение трейт-объектов (
Box<dyn Trait>): Позволяет работать с динамическим полиморфизмом. - Стабильный адрес объекта: Сам
Box<T>можно перемещать по стеку, при этом сам объектTв куче остается на том же месте в памяти.
struct BigData {
values: [u8; 10000],
}
fn main() {
// Данные размещаются в куче
let mut boxed_data = Box::new(BigData { values: [0; 10000] });
// Модифицируем данные по указателю
boxed_data.values[0] = 42;
} // При выходе boxed_data из scope освобождается память в куче!
Отмена деаллокации: Box::leak#
Иногда требуется зарегистрировать динамически созданную конфигурацию или данные, которые должны жить в течение всего времени работы программы со ссылкой 'static.
Для этого существует метод Box::leak:
struct GlobalConfig {
port: u16,
}
fn main() {
let config = Box::new(GlobalConfig { port: 8080 });
// Забирает владение Box<T> и возвращает ссылку на объект
let static_ref: &'static mut GlobalConfig = Box::leak(config);
static_ref.port = 9090;
}Box::leak забирает владение Box<T> и возвращает ссылку на объект в куче. Автоматически освободить этот объект после этого уже нельзя. Полученную ссылку можно использовать с временем жизни 'static или более коротким.
Обратите внимание: 'static здесь относится именно к времени жизни возвращаемой ссылки, а не означает, что объект обязан физически существовать ровно до завершения процесса.
Наличие &mut при этом не отменяет стандартных правил заимствования Rust (алиасинг по-прежнему запрещен). На практике для многопоточных синглтонов чаще используют типы OnceLock или LazyLock, а Box::leak применяется как низкоуровневый инструмент передачи данных в API с требованиями 'static.
3. Трейты Deref и DerefMut#
Как сделать так, чтобы умный указатель ощущался в коде так же удобно, как обычная ссылка? Для этого в модуле std::ops предназначены типажи Deref и DerefMut.
При разыменовании *my_box Rust использует реализацию Deref, концептуально получая ссылку через my_box.deref().
use std::ops::{Deref, DerefMut};
struct MyBox<T>(T);
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
fn print_value(value: &i32) {
println!("Значение: {value}");
}
fn main() {
let boxed_val = MyBox(42);
// Rust автоматически использует Deref для передачи ссылки
print_value(&boxed_val);
}4. Механизм Deref Coercion (Автоматическое приведение ссылок)#
Мы реализовали Deref - и передача &MyBox<i32> в функцию, ожидающую &i32, сработала автоматически. Но как именно компилятор понимает, что одну ссылку можно передать вместо другой?
Здесь на сцену выходит механизм Deref Coercion. Если тип T реализует Deref<Target = U>, компилятор умеет автоматически приводить ссылки вида &T к ссылкам &U.
Важно подчеркнуть: Deref Coercion работает строго со ссылками, а не преобразует сам Box<T> по значению.
Три правила приведения ссылок:#
- Из
&Tв&U, еслиT: Deref<Target = U>(неизменяемая ссылка в неизменяемую). - Из
&mut Tв&mut U, еслиT: DerefMut<Target = U>(изменяемая ссылка в изменяемую). - Из
&mut Tв&U, еслиT: Deref<Target = U>(из изменяемой в неизменяемую).
Приведение из &T в &mut U запрещено фундаментально - оно нарушило бы ключевые гарантии алиасинга и безопасности памяти в Rust.
Цепочки приведений (Chain Coercion)#
Компилятор умеет применять Deref цепочкой несколько раз подряд. Рассмотрим тип Box<String>:
&Box<String>
↓ Deref
&String
↓ Deref
&strБлагодаря этой цепочке при передаче &Box<String> в функцию, принимающую &str, Rust прозрачно приведёт ссылки без явных вызовов:
fn print_greeting(name: &str) {
println!("Привет, {name}!");
}
fn main() {
let boxed_name = Box::new(String::from("Алексей"));
// Автоматическая цепочка Deref Coercion: &Box<String> -> &String -> &str
print_greeting(&boxed_name);
// Благодаря Deref Rust умеет искать подходящие методы и у целевого типа:
println!("Длина имени: {}", boxed_name.len());
}Благодаря Deref Rust умеет искать подходящие методы и у целевого типа. Поэтому boxed_name.len() работает так же естественно, как вызов len() у String или str.
Давайте изучим эти механизмы на интерактивных слайдах:
Проверь свои знания!#
Пройдите короткий тест, чтобы закрепить понимание идиомы RAII, работы Box<T>


