Выполнение кода при очистке с помощью трейта Drop
Второй трейт, важный для паттерна умного указателя, – это Drop. Он
позволяет настраивать, что происходит, когда значение вот-вот выйдет из
области видимости. Вы можете предоставить реализацию трейта Drop для любого
типа, и этот код можно использовать для освобождения ресурсов, таких как файлы
или сетевые соединения.
Мы вводим Drop в контексте умных указателей, потому что функциональность
трейта Drop почти всегда используется при реализации умного указателя.
Например, когда Box<T> удаляется, он освобождает место в куче, на которое
указывает box.
В некоторых языках для некоторых типов программист должен вызывать код освобождения памяти или ресурсов каждый раз, когда заканчивает использовать экземпляр этих типов. Примеры включают файловые дескрипторы, сокеты и блокировки. Если программист забудет это сделать, система может оказаться перегруженной и аварийно завершить работу. В 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");
}
CustomSmartPointer, реализующая трейт Drop, где мы разместили бы код очисткиТрейт Drop включен в прелюдию, поэтому нам не нужно вводить его в область
видимости. Мы реализуем трейт 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");
}
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.
Сообщение об ошибке использует термин деструктор, который в программировании
в общем смысле означает функцию, очищающую экземпляр. Деструктор аналогичен
конструктору, который создает экземпляр. Функция drop в Rust – один
конкретный деструктор.
Rust не позволяет явно вызывать drop, потому что Rust все равно
автоматически вызвал бы drop для значения в конце main. Это привело бы к
ошибке двойного освобождения, потому что 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, показывая, что код метода drop вызывается для удаления c в
этот момент.
Вы можете разными способами использовать код, указанный в реализации трейта
Drop, чтобы сделать очистку удобной и безопасной: например, с его помощью
можно создать собственный распределитель памяти! С трейтом Drop и системой
владения Rust вам не нужно помнить об очистке, потому что Rust делает это
автоматически.
Вам также не нужно беспокоиться о проблемах, возникающих из-за случайной
очистки значений, которые все еще используются: система владения, которая
следит за тем, чтобы ссылки всегда были допустимыми, также гарантирует, что
drop вызывается только один раз, когда значение больше не используется.
Теперь, когда мы рассмотрели Box<T> и некоторые характеристики умных
указателей, посмотрим на еще несколько умных указателей, определенных в
стандартной библиотеке.