Выполнение кода при очистке с помощью трейта Drop
Вторым трейтом, важным для шаблона умных указателей, является Drop, который позволяет вам настраивать то, что происходит, когда значение вот-вот выйдет из области видимости. Вы можете предоставить реализацию трейта Drop для любого типа, и этот код может использоваться для освобождения ресурсов, таких как файлы или сетевые соединения.
Мы представляем Drop в контексте умных указателей, потому что функциональность трейта Drop почти всегда используется при реализации умного указателя. Например, когда Box<T> удаляется, он освобождает пространство в куче, на которое указывает бокс.
В некоторых языках для некоторых типов программист должен вызывать код для освобождения памяти или ресурсов каждый раз, когда он заканчивает использование экземпляра этих типов. Примеры включают файловые дескрипторы, сокеты и блокировки. Если программист забудет, система может перегрузиться и аварийно завершиться. В Rust вы можете указать, что определённый код должен выполняться всякий раз, когда значение выходит из области видимости, и компилятор вставит этот код автоматически. В результате вам не нужно быть осторожным с размещением кода очистки везде в программе, где завершается использование экземпляра определённого типа — вы всё равно не будете течь ресурсами!
Вы указываете код для выполнения при выходе значения из области видимости, реализуя трейт Drop. Трейт Drop требует, чтобы вы реализовали один метод с именем drop, который принимает изменяемую ссылку на self. Чтобы увидеть, когда Rust вызывает drop, давайте пока реализуем 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"); }
Трейт Drop включён в прелюдию (prelude), поэтому нам не нужно вносить его в область видимости. Мы реализуем трейт Drop для CustomSmartPointer и предоставляем реализацию метода drop, которая вызывает println!. Тело метода drop — это место, куда вы поместили бы любую логику, которую хотите выполнить, когда экземпляр вашего типа выходит из области видимости. Мы печатаем здесь некоторый текст, чтобы визуально продемонстрировать, когда Rust вызовет drop.
В main мы создаём два экземпляра CustomSmartPointer, а затем печатаем CustomSmartPointers created. В конце main наши экземпляры CustomSmartPointer выйдут из области видимости, и 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, который освобождает блокировку, чтобы другой код в той же области видимости мог получить блокировку. Rust не позволяет вам вручную вызывать метод drop трейта Drop; вместо этого, если вы хотите принудительно удалить значение до конца его области видимости, вы должны вызвать функцию std::mem::drop, предоставляемую стандартной библиотекой.
Попытка вручную вызвать метод drop трейта Drop путём изменения функции main из Листинга 15-14 не сработает, как показано в Листинге 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");
}
Когда мы попытаемся скомпилировать этот код, мы получим эту ошибку:
$ 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, потому что Rust всё равно автоматически вызовет drop для значения в конце main. Это вызвало бы ошибку двойного освобождения (double free error), потому что 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"); }
Запуск этого кода напечатает следующее:
$ 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, показывая, что код метода drop вызывается для удаления c в этот момент.
Вы можете использовать код, указанный в реализации трейта Drop, многими способами, чтобы сделать очистку удобной и безопасной: например, вы могли бы использовать его для создания собственного аллокатора памяти! С трейтом Drop и системой владения Rust вам не нужно помнить об очистке, потому что Rust делает это автоматически.
Вам также не нужно беспокоиться о проблемах, возникающих из-за случайной очистки значений, всё ещё используемых: система владения, которая гарантирует, что ссылки всегда действительны, также обеспечивает, чтобы drop вызывался только один раз, когда значение больше не используется.
Теперь, когда мы изучили Box<T> и некоторые характеристики умных указателей, давайте посмотрим на несколько других умных указателей, определённых в стандартной библиотеке.