デストラクタでのファイナライズ
説明
Rust には finally
ブロック (関数の終了の仕方を問わずに実行されるコード) に相当するものがありません。その代わりに、オブジェクトのデストラクタを使用して、終了前に実行しなければならないコードを実行させることができます。
例
fn bar() -> Result<(), ()> {
// これらは関数内部で定義する必要はない。
struct Foo;
// Foo のデストラクタを実装する。
impl Drop for Foo {
fn drop(&mut self) {
println!("exit");
}
}
// _exit のデストラクタは、関数`bar`が終了しても実行される。
let _exit = Foo;
// 演算子 `?` による暗黙的なリターン 。
baz()?;
// 通常のリターン。
Ok(())
}
動機形成
関数に複数のリターン・ポイントがある場合、関数終了時にコードを実行するのは難しく、繰り返し的になります(またそれゆえにバグの原因になります)。リターンがマクロによって暗黙的に行われる場合、特にそうなります。よくあるケースが ?
演算子で、これは結果が Err
なら戻り、Ok
なら続行するものです。 ?
は例外処理のメカニズムとして使用されますが、(finally
を持つ) Javaとは異なり、通常と例外の両方のケースで実行されるコードを実装する方法はありません。また、パニックが起きると関数は早期に終了します。
長所
デストラクタ内のコードは (ほぼ) 常に実行されます。パニックや早期リターンなどにも対処できます。
短所
デストラクタの実行は保証されていません。例えば関数の無限ループがある場合や、関数の実行が終了前にクラッシュした場合などです。また、すでにパニックになっているスレッドでパニックが発生した場合にも、デストラクタは実行されません。したがって、絶対的かつ本質的にファイナライズを行うファイナライザとしては、デストラクタを頼ることはできません。
このパターンは、気づきにくい暗黙のコードを導入することになります。関数を読んでも、 終了時にデストラクタが実行されることについて、明確な兆候は得られません。これはデバッグを厄介なものにする可能性があります。
ファイナライズのためだけにオブジェクトと Drop
の実装が必要となり、ボイラーテンプレートが多くなります。
議論
ファイナライザとして使用されるオブジェクトの正確な格納方法については、繊細な点があります。このオブジェクトは関数が終了するまで存続しなければならず、また終了時に破棄されなければなりません。このオブジェクトは常に値かユニークに所有されるポインタでなければなりません (例 Box<Foo>
など) 。もし共有ポインタ(Rc
など)を使用した場合、ファイナライザが関数のライフタイムを超えて存続してしまう可能性があります。同様の理由でファイナライザの所有権を移動したり、ファイナライザを戻り値としてはいけません。
ファイナライザは変数に代入しなければなりません。そうでないとスコープを外れる前に即座に破棄されます。変数名は _
で始まらなければなりません。そうでないとコンパイラは変数が利用されていない旨の警告を出します。しかし変数をサフィックスなしの _
としてはいけません。そうすると、即座に破棄されてしまいます。
Rustでは、デストラクタはオブジェクトがスコープ外に出たときに実行されます。これが起こるのは、ブロックの終了に到達したとき、早期リターンのとき、パニックが発生したときです。パニックが起きると、Rustはスタックフレームの各オブジェクトのデストラクタを実行しつつスタックを巻き戻します。そのため、呼び出し中の関数内にてパニックが発生した場合でも、デストラクタが呼び出されます。
デストラクタが巻き戻し中にパニックを起こした場合、取るべき良いアクションはないため、Rustはそれ以上デストラクタを実行せずにスレッドを即座に終了させます。これは、デストラクタの実行が絶対に保証されているわけではないということです。また、デストラクタがパニックに陥らないように細心の注意を払う必要があるということでもあります。リソースを想定されない状態にする可能性があるからです。