Запуск кода при очистке с помощью типажа Drop

Вторым важным типажом умного указателя является Drop, который позволяет регулировать, что происходит, когда значение вот-вот выйдет из области видимости. Вы можете реализовать типаж Drop для любого типа, а также использовать этот код для высвобождения ресурсов, таких как файлы или сетевые соединения.

Мы рассматриваем Drop в контексте умных указателей, потому что функциональность свойства Drop практически всегда используется при реализации умного указателя. Например, при сбросе Box<T> происходит деаллокация пространства на куче, на которое указывает box.

В некоторых языках для некоторых типов программист должен вызывать код для освобождения памяти или ресурсов каждый раз, когда он завершает использование экземпляров этих типов. Примерами могут служить дескрипторы файлов, сокеты или блокировки. Если забыть об этом, система окажется перегруженной и может упасть. В Rust вы можете указать, что определённый фрагмент кода должен выполняться всякий раз, когда значение выходит из области видимости, и компилятор автоматически будет его вставлять. Как следствие, вам не нужно заботиться о размещении кода очистки везде в программе, где завершается работа экземпляра определённого типа - утечки ресурсов все равно не будет!

Вы можете задать определённую логику, которая будет выполняться, когда значение выходит за пределы области видимости, реализовав признак Drop. Типаж Drop требует от вас реализации одного метода drop, который принимает изменяемую ссылку на self. Чтобы увидеть, когда Rust вызывает drop, давайте реализуем drop с помощью инструкций println!.

В листинге 15-14 показана структура CustomSmartPointer, единственной уникальной функциональностью которой является печать Dropping CustomSmartPointer!, когда экземпляр выходит из области видимости, чтобы показать, когда Rust выполняет функцию drop.

Файл: src/main.rs

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.");
}

Листинг 15-14: Структура 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 [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; в типичных случаях вы будете задавать код очистки, который должен выполнить ваш тип, а не печатать сообщение.

Раннее удаление значения с помощью std::mem::drop

К сожалению, отключение функции автоматического удаления с помощью drop является не простым. Отключение drop обычно не требуется; весь смысл типажа Drop в том, чтобы о функции позаботились автоматически. Иногда, однако, вы можете захотеть очистить значение рано. Одним из примеров является использование умных указателей, которые управляют блокировками: вы могли бы потребовать принудительный вызов метода drop который снимает блокировку, чтобы другой код в той же области видимости мог получить блокировку. Rust не позволяет вызвать метод типажа Drop вручную; вместо этого вы должны вызвать функцию std::mem::drop предоставляемую стандартной библиотекой, если хотите принудительно удалить значение до конца области видимости.

Если попытаться вызвать метод drop типажа Drop вручную, изменяя функцию main листинга 15-14 так, как показано в листинге 15-15, мы получим ошибку компилятора:

Файл: src/main.rs

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.");
}

Листинг 15-15: Попытка вызвать метод 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 |     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 напрямую, потому что он все равно автоматически вызовет drop в конце main. Это вызвало бы ошибку double free, потому что в этом случае Rust попытался бы дважды очистить одно и то же значение.

Невозможно отключить автоматическую подстановку вызова drop, когда значение выходит из области видимости, и нельзя вызвать метод drop напрямую. Поэтому, если нам нужно принудительно избавиться от значения раньше времени, следует использовать функцию std::mem::drop.

Функция std::mem::drop отличается от метода drop трейта Drop. Мы вызываем её, передавая в качестве аргумента значение, которое хотим принудительно уничтожить. Функция находится в прелюдии, поэтому мы можем изменить main в листинге 15-15 так, чтобы вызвать функцию drop, как показано в листинге 15-16:

Файл: src/main.rs

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.");
}

Листинг 15-16: Вызов std::mem::drop для принудительного удаления значения до того, как оно выйдет из области видимости

Выполнение данного кода выведет следующий результат::

$ cargo run
   Compiling drop-example v0.1.0 (file:///projects/drop-example)
    Finished dev [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> и характеристиками умных указателей, познакомимся с другими умными указателями, определёнными в стандартной библиотеке.