Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Код при очистке: трейт Drop

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

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

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

Код, который выполняется при выходе значения из области видимости, задаётся реализацией трейта Drop. Трейт Drop требует реализовать один метод — drop, который принимает изменяемую ссылку на self. Чтобы увидеть, когда Rust вызывает 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 наши экземпляры выйдут из области видимости, и 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.

Файл: 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 -     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.

Файл: 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` 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> и некоторые особенности умных указателей, посмотрим на несколько других умных указателей из стандартной библиотеки.