Код при очистке: трейт Drop
Второй трейт, важный для умных указателей, — Drop. Он позволяет задать, что
происходит, когда значение вот-вот выйдет из области видимости. Реализовать
трейт Drop можно для любого типа, а в его реализации — освобождать ресурсы
вроде файлов или сетевых соединений.
Мы рассказываем о Drop в контексте умных указателей потому, что его
функциональность почти всегда используется при реализации умного указателя.
Например, когда уничтожается Box<T>, он освобождает место в куче, на которое
указывает.
В некоторых языках для некоторых типов программист должен каждый раз, закончив работу с экземпляром, вызывать код, освобождающий память или ресурсы. Примеры — дескрипторы файлов, сокеты, блокировки. Если программист забудет, система может оказаться перегруженной и упасть. В Rust можно указать код, который будет выполняться всякий раз, когда значение выходит из области видимости, и компилятор вставит его автоматически. В итоге не нужно следить за тем, чтобы расставить код очистки везде, где программа заканчивает работу с экземпляром определённого типа, — ресурсы всё равно не утекут!
Код, который выполняется при выходе значения из области видимости, задаётся
реализацией трейта Drop. Трейт Drop требует реализовать один метод —
drop, который принимает изменяемую ссылку на self. Чтобы увидеть, когда Rust
вызывает drop, пока реализуем его с помощью println!.
В листинге 15-14 — структура CustomSmartPointer, вся особая
функциональность которой в том, что при выходе экземпляра из области видимости
она выводит Dropping CustomSmartPointer! — так видно, когда Rust вызывает
метод drop.
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer {
data: String::from("my stuff"),
};
let d = CustomSmartPointer {
data: String::from("other stuff"),
};
println!("CustomSmartPointers created");
}
CustomSmartPointer, реализующая трейт Drop, — сюда мы поместили бы код очисткиТрейт Drop входит в прелюдию, так что подключать его не нужно. Мы реализуем
Drop для CustomSmartPointer, а в методе drop вызываем println!. В тело
метода drop помещают любую логику, которая должна выполняться, когда
экземпляр вашего типа выходит из области видимости. Здесь мы просто выводим
текст, чтобы наглядно показать, когда Rust вызывает drop.
В main мы создаём два экземпляра CustomSmartPointer и выводим
CustomSmartPointers created. В конце main наши экземпляры выйдут из
области видимости, и Rust выполнит код из метода drop, выведя финальное
сообщение. Заметьте, что вызывать drop явно нам не пришлось.
Запустив программу, увидим:
$ cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.60s
Running `target/debug/drop-example`
CustomSmartPointers created
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!
Когда экземпляры вышли из области видимости, Rust сам вызвал drop и выполнил
наш код. Переменные уничтожаются в порядке, обратном созданию, поэтому d был
уничтожен раньше c. Этот пример лишь наглядно показывает, как работает метод
drop; обычно в нём пишут код очистки, нужный вашему типу, а не вывод
сообщения.
К сожалению, отключить автоматический вызов drop не так просто. Обычно это и
не нужно: весь смысл трейта Drop в том, что о нём заботятся автоматически.
Но иногда значение хочется освободить пораньше. Например, когда умные
указатели управляют блокировками: может понадобиться принудительно вызвать
drop, который снимает блокировку, чтобы другой код в той же области
видимости мог её захватить. Вызывать метод drop трейта Drop вручную Rust не
позволяет. Если нужно принудительно уничтожить значение раньше конца его
области видимости, вызывайте функцию std::mem::drop из стандартной
библиотеки.
Если изменить функцию main из листинга 15-14 и попытаться вручную вызвать
метод drop трейта Drop, ничего не выйдет — см. листинг 15-15.
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer {
data: String::from("some data"),
};
println!("CustomSmartPointer created");
c.drop();
println!("CustomSmartPointer dropped before the end of main");
}
drop трейта Drop, чтобы очистить значение досрочноПри компиляции получим ошибку:
$ cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example)
error[E0040]: explicit use of destructor method
--> src/main.rs:16:7
|
16 | c.drop();
| ^^^^ explicit destructor calls not allowed
|
help: consider using `drop` function
|
16 - c.drop();
16 + drop(c);
|
For more information about this error, try `rustc --explain E0040`.
error: could not compile `drop-example` (bin "drop-example") due to 1 previous error
Сообщение говорит, что вызывать drop явно нельзя. В нём используется термин
деструктор (destructor) — общепринятое в программировании название функции,
которая очищает экземпляр. Деструктор — пара к конструктору, который
экземпляр создаёт. Функция drop в Rust — один из видов деструктора.
Rust не даёт вызывать drop явно, потому что в конце main он всё равно
автоматически вызвал бы drop для этого значения. Получилась бы ошибка
двойного освобождения: Rust попытался бы очистить одно и то же значение
дважды.
Отключить автоматическую вставку drop при выходе значения из области
видимости нельзя, вызвать метод drop явно — тоже. Поэтому, если значение
нужно очистить досрочно, используют функцию std::mem::drop.
Функция std::mem::drop — это не метод drop трейта Drop. Её вызывают,
передавая аргументом значение, которое нужно принудительно уничтожить. Эта
функция есть в прелюдии, так что main из листинга 15-15 можно изменить так,
чтобы она вызывала функцию drop, как в листинге 15-16.
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer {
data: String::from("some data"),
};
println!("CustomSmartPointer created");
drop(c);
println!("CustomSmartPointer dropped before the end of main");
}
std::mem::drop, чтобы явно уничтожить значение до выхода из области видимостиЭтот код выведет:
$ cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/drop-example`
CustomSmartPointer created
Dropping CustomSmartPointer with data `some data`!
CustomSmartPointer dropped before the end of main
Текст Dropping CustomSmartPointer with data `some data`! выводится между
CustomSmartPointer created и
CustomSmartPointer dropped before the end of main. Значит, c уничтожается
именно в этот момент — тогда и выполняется код метода drop.
Код из реализации трейта Drop можно использовать по-разному, чтобы очистка
была удобной и безопасной: например, на его основе можно написать собственный
распределитель памяти! С трейтом Drop и системой владения Rust помнить об
очистке не нужно — Rust делает это автоматически.
Не нужно беспокоиться и о проблемах из-за случайной очистки значений, которые
ещё используются: система владения, гарантирующая, что ссылки всегда
действительны, гарантирует и то, что drop будет вызван лишь один раз — когда
значение больше не используется.
Теперь, когда мы разобрали Box<T> и некоторые особенности умных указателей,
посмотрим на несколько других умных указателей из стандартной библиотеки.