デストラクタでのファイナライゼーション
説明
Rustはfinallyブロックに相当するもの、つまり関数がどのように終了してもコードが実行されることを保証する仕組みを提供していません。その代わりに、オブジェクトのデストラクタを使用して、終了前に実行する必要があるコードを実行できます。
例
fn baz() -> Result<(), ()> {
// some code
}
fn bar() -> Result<(), ()> {
// These don't need to be defined inside the function.
struct Foo;
// Implement a destructor for Foo.
impl Drop for Foo {
fn drop(&mut self) {
println!("exit");
}
}
// The dtor of _exit will run however the function `bar` is exited.
let _exit = Foo;
// Implicit return with `?` operator.
baz()?;
// Normal return.
Ok(())
}
動機
関数に複数のreturnポイントがある場合、終了時にコードを実行することが困難で繰り返しになり(したがってバグが発生しやすくなります)。これは、マクロによってreturnが暗黙的に行われる場合に特に当てはまります。よくあるケースは?演算子で、結果がErrの場合はreturnし、Okの場合は続行します。?は例外処理機構として使用されますが、Javaと異なり(Javaにはfinallyがあります)、通常のケースと例外的なケースの両方で実行されるコードをスケジュールする方法がありません。panicも関数を早期に終了させます。
利点
デストラクタ内のコードは(ほぼ)常に実行されます - panic、早期return等に対処できます。
欠点
デストラクタが実行されることは保証されていません。例えば、関数内に無限ループがある場合や、関数の実行が終了前にクラッシュした場合です。デストラクタは、すでにpanicしているスレッド内でpanicが発生した場合にも実行されません。したがって、ファイナライゼーションが絶対的に必要な場合、デストラクタをファイナライザーとして信頼することはできません。
このパターンは、気づきにくい暗黙的なコードを導入します。関数を読んでも、終了時に実行されるデストラクタの明確な兆候がありません。これはデバッグを難しくする可能性があります。
ファイナライゼーションのためだけにオブジェクトとDrop実装を必要とすることは、定型コードが多くなります。
議論
ファイナライザーとして使用されるオブジェクトをどのように正確に格納するかについては、いくつかの微妙な点があります。オブジェクトは関数の終わりまで生きていなければならず、その後破棄される必要があります。オブジェクトは常に値または一意に所有されたポインタ(例:Box<Foo>)でなければなりません。共有ポインタ(Rcなど)を使用すると、ファイナライザーは関数の生存期間を超えて生きることができます。同様の理由で、ファイナライザーは移動したり返したりしてはいけません。
ファイナライザーは変数に割り当てる必要があります。そうしないと、スコープから外れるときではなく、すぐに破棄されます。変数がファイナライザーとしてのみ使用される場合、変数名は_で始まる必要があります。そうしないと、コンパイラはファイナライザーが使用されていないと警告します。ただし、変数を接尾辞なしの_と呼ばないでください - その場合はすぐに破棄されます。
Rustでは、デストラクタはオブジェクトがスコープから外れるときに実行されます。これは、ブロックの終わりに到達した場合、早期returnがある場合、またはプログラムがpanicした場合に発生します。panicすると、Rustはスタックを巻き戻し、各スタックフレーム内の各オブジェクトのデストラクタを実行します。したがって、デストラクタは、呼び出されている関数内でpanicが発生した場合でも呼び出されます。
巻き戻し中にデストラクタがpanicした場合、取るべき良いアクションがないため、Rustはさらなるデストラクタを実行せずに、スレッドを直ちに中止します。これは、デストラクタが絶対的に実行されることが保証されていないことを意味します。また、デストラクタがpanicしないように特別な注意を払う必要があることも意味します。リソースが予期しない状態になる可能性があるためです。