デストラクタ
言語が実際に提供しているものは、 Drop
トレイトを通じた完全に自動的な
デストラクタで、以下のメソッドを提供しています。
fn drop(&mut self);
このメソッドは、型が行なっていたことをなんとか終わらせるための時間を、型に 与えます。
drop
が実行された後、 Rust は self
の全てのフィールドのドロップを再帰的に実行しようとします。
これは便利な機能で、子フィールドをドロップするための "デストラクタの決まり文句" を
書く必要がありません。もし構造体に、子フィールドをドロップする以外の、ドロップされる際の
特別なロジックが存在しなければ、 Drop
を実装する必要が全くありません!
この振る舞いを防ぐステーブルな方法は、 Rust 1.0 の時点で存在しません
&mut self
を受け取ることは、再帰ドロップを防ぐことが出来たとしても、例えば self から
フィールドをムーブすることが妨げられることに注意してください。
ほとんどの型にとっては、全く問題ありません。
例えば Box
のカスタム実装では、以下のような Drop
を書くかもしれません。
#![feature(alloc, heap_api, unique)] extern crate alloc; use std::ptr::{drop_in_place, Unique}; use std::mem; use alloc::heap; struct Box<T>{ ptr: Unique<T> } impl<T> Drop for Box<T> { fn drop(&mut self) { unsafe { drop_in_place(*self.ptr); heap::deallocate((*self.ptr) as *mut u8, mem::size_of::<T>(), mem::align_of::<T>()); } } } fn main() {}
そしてこれは、 Rust が ptr
フィールドをドロップする際、単に、実際の Drop
実装が
ない Unique に着目するため、このコードは問題なく動くのです。
同様に、解放後は ptr
を使用することが出来ません。なぜならドロップが存在する場合、
そのドロップ実装にアクセス不可能となるからです。
しかし、このコードは動かないでしょう。
#![feature(alloc, heap_api, unique)] extern crate alloc; use std::ptr::{drop_in_place, Unique}; use std::mem; use alloc::heap; struct Box<T>{ ptr: Unique<T> } impl<T> Drop for Box<T> { fn drop(&mut self) { unsafe { drop_in_place(*self.ptr); heap::deallocate((*self.ptr) as *mut u8, mem::size_of::<T>(), mem::align_of::<T>()); } } } struct SuperBox<T> { my_box: Box<T> } impl<T> Drop for SuperBox<T> { fn drop(&mut self) { unsafe { // 超最適化: Box の内容を `drop` せずに // 内容をデアロケートします heap::deallocate((*self.my_box.ptr) as *mut u8, mem::size_of::<T>(), mem::align_of::<T>()); } } } fn main() {}
SuperBox のデストラクタで box
の ptr をデアロケートした後、 Rust は適切に box に、
自身をドロップするよう通達し、その結果、解放後の使用や二重解放によって全部消し飛びます。
再帰ドロップは、構造体や列挙型が Drop を定義しているかしていないかによらず、 全ての構造体や列挙型に適用されることに注意してください。 ですから、以下のような
#![allow(unused)] fn main() { struct Boxy<T> { data1: Box<T>, data2: Box<T>, info: u32, } }
ものは、それ自体が Drop を実装していなくても、それがドロップされるときには毎回、 data1 と data2 の フィールドをデストラクトします。これを、そのような型が Drop を必要とすると言います。型が Drop を 実装していなくてもです。
同様に
#![allow(unused)] fn main() { enum Link { Next(Box<Link>), None, } }
これは、インスタンスが Next を格納しているとき、そのときだけ内部の Box フィールドを ドロップします。
一般に、これは非常に上手く動きます。なぜなら、データレイアウトをリファクタリングするときに、 ドロップを追加あるいは削除する心配が必要ないからです。もちろん、デストラクタで何か トリッキーなことが必要になる妥当なケースは、たくさんあります。
再帰ドロップを上書きし、 drop
の最中に Self からのムーブを可能にする、
古典的で安全な解決策は、 Option を使うことです。
#![feature(alloc, heap_api, unique)] extern crate alloc; use std::ptr::{drop_in_place, Unique}; use std::mem; use alloc::heap; struct Box<T>{ ptr: Unique<T> } impl<T> Drop for Box<T> { fn drop(&mut self) { unsafe { drop_in_place(*self.ptr); heap::deallocate((*self.ptr) as *mut u8, mem::size_of::<T>(), mem::align_of::<T>()); } } } struct SuperBox<T> { my_box: Option<Box<T>> } impl<T> Drop for SuperBox<T> { fn drop(&mut self) { unsafe { // 超最適化: Box の内容を `drop` せずに // 内容をデアロケートします // Rust が `box` フィールドをドロップしようとさせないために、 // `box` フィールドを `None` と設定する必要があります let my_box = self.my_box.take().unwrap(); heap::deallocate((*my_box.ptr) as *mut u8, mem::size_of::<T>(), mem::align_of::<T>()); mem::forget(my_box); } } } fn main() {}
しかしながら、これはかなり奇妙なセマンティクスです。すなわち、常に Some であるべき フィールドが、 None になりうると言っているからです。なぜならこれが、 デストラクタで起こっているからです。勿論、これは逆に大いに納得がいきます。 デストラクタ内で self に対して任意のメソッドを呼ぶことができ、同じことが、 フィールドが未初期化状態に戻されたあとに行われるのを防ぐはずですから。 だからといって、他の不正状態が生まれることを防ぐわけではありませんが。
結局のところ、これはそれほど悪くないやり方です。明らかに誰もが必要とする機能です。 しかしながら将来、あるフィールドが自動的にドロップされるべきでないと知らせる、 素晴らしい方法が現れると我々は期待しています。