Виконання коду при очищенні за допомогою трейту Drop

Другий важливий трейт для шаблону розумного вказівника - Drop, який дозволяє вам налаштувати, що відбувається, коли значення збирається вийти з області видимості. Ви можете створити реалізацію трейту Drop для будь-якого типу, і цей код може використовуватися для звільнення ресурсів на кшталт файлів чи мережевих з'єднань.

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

У деяких мовах для деяких типів програміст повинен викликати код для звільнення пам'яті або ресурсів кожного разу, коли він завершує використовувати екземпляр такого типу. Прикладами можуть бути файли, сокети чи блокування доступу до даних. Якщо програміст забуде це зробити, система може перенавантажитися і впасти. У 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 трейту 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: `drop(c)`

For more information about this error, try `rustc --explain E0040`.
error: could not compile `drop-example` due to 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:

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

Текст Видалення CustomSmartPointer з даними `деякі дані`! виводиться між текстами CustomSmartPointer created. і CustomSmartPointer dropped before the end of main., показуючи, що код методу drop викликається для очищення с в цьому місці.

Ви можете використати код, вказаний у реалізації трейту Drop, у різні способи, щоб зробити очищення зручним і безпечним: зокрема, ви могли б використати його для створення власного розподілювача пам'яті! Завдяки трейту Drop і системі володіння Rust, вам не треба пам'ятати про очищення, бо Rust робить це автоматично.

Також вам не доведеться турбуватися про проблеми, що можуть виникнути через випадкове очищення значень, які все ще використовуються: система власності, яка гарантує, що посилання завжди дійсні, також гарантує, що метод drop буде викликаний лише один раз, коли значення більше не використовуватиметься.

Тепер, коли ми дослідили Box<T> і деякі характеристики розумних вказівників, погляньмо на деякі інші розумні вказівники, визначені у стандартній бібліотеці.