巻き戻し

Rustのエラーハンドリングには階層的なスキームが存在します。

  • もし何かが、明確な理由があって欠如しうる場合、Optionが使われます
  • もし何かおかしなことが起こった際に合理的な対処方法がある場合、Resultが使われます
  • もし何かおかしなことが起こった際に合理的な対処方法がない場合、そのスレッドはパニックします
  • もし何か破滅的な出来事が起こった場合、プログラムはアボートします

大抵の状況では圧倒的にOptionとResultが好まれます。というのもAPIのユーザーの 裁量次第でパニックやアボートさせることも可能だからです。パニックはスレッドの正常処理を 停止し、スタックを巻き戻し、全ての関数が即座にリターンしたかのようにデストラクタ を呼び出します。

バージョン1.0以降のRustはパニック時に2種類の対処法を用いるようになりました。 大昔、Rustは今よりもErlangによく似ていました。Erlangと同様、Rustには軽量のタスク が存在し、タスクが続行不可能な状態に陥った際にはタスクが自分自身をパニックによって killすることを意図して設計されていました。JavaやC++の例外と違い、パニックはいかなる 場合においても捕捉することはできませんでした。パニックを捕捉できるのはタスクの オーナーのみであり、その時点で適切にハンドリングされるか、そのタスク (訳注: オーナーとなるタスク)自体がパニックするかのどちらかでした。

この一連の流れの中では、タスクのデスクトラクタが呼ばれなかった場合にメモリ及び その他のシステムリソースがリークを起こす可能性があったため、巻き戻しが重要でした。 タスクは通常の実行中にも死ぬ可能性があると想定されていたため、Rustのこういった 特徴は長期間実行されるシステムを作る上でとても不適切でした。

Rustが現在の形に近づく過程で、より抽象化を少なくしたいという時流に押された スタイルのプログラミングが確立していき、その過程で軽量のタスクは重量級の OSスレッドに駆逐・統一されました (訳注: いわゆるグリーンスレッドとネイティブスレッドの話)。しかしながら Rust1.0の時点ではパニックはその親スレッドによってのみ補足が可能という仕様であった ため、 パニックの補足時にOSのスレッドを丸ごと巻き戻してしまう必要 があったのです!不幸なことにこれはゼロコスト抽象化というRustの思想と 真っ向からぶつかってしまいました。

一応 catch_panic というunstableなAPIが存在し、これによってスレッドをspawn することなくパニックを捕捉することはできます。

訳注: その後 recover -> catch_unwind と変更され、Rust1.9でstableになりました。

とはいえあくまでこれは代替手段として用いることを推奨します。現在のRustの巻き戻し は「巻き戻ししない」ケースに偏った最適化をしています。巻き戻しが発生しないとわかって いれば、プログラムが巻き戻しの準備をするためのランタイムコストも無くなるためです。 結果として、実際にはJavaのような言語よりも巻き戻しのコストは高くなっています。 したがって通常の状況では巻き戻ししないようなプログラムの作成を心がけるべきです。 非常に大きな問題の発生時やプログラミングエラーに対してのみパニックすべきです。

Rustの巻き戻しの取り扱い方針は、他の言語のそれと根本から同等になるように設計されて はいません。したがって他の言語で発生した巻き戻しがRustに波及したり、逆にRustから 多言語に波及したりといった動作は未定義となっています。 FFIの構築時には絶対に全てのパニックを境界部でキャッチしなくてはなりません。 キャッチの結果どのように対処するかはプログラマ次第ですが、とにかく何かを しなくてはなりません。そうしなければ、良くてアプリケーションがクラッシュ・炎上します。 最悪のケースではアプリケーションがクラッシュ・炎上しません。完全にボロボロの状態 のまま走り続けます。