TL;DR
panic!を使うより、Resultを使う。catch_unwindで例外を真似るのは、abort される場合があること、例外安全性、FFI の問題から避ける。- パニックは
panic!以外からも起きるので、人の目ではなく CI で検出する。
内容
https://effective-rust.com/panic.html
panic! を使うよりも、Result を使おう
panic で例外を真似ない
パニックは、プログラム中の回復不能なバグに対処するための仕組みである。そのため、デフォルトではパニックしたスレッドを停止させる。
一見すると、std::panic::catch_unwind を使えば例外の仕組みを真似できそうに見える。
fn divide_recover(a: i64, b: i64, default: i64) -> i64 {
let result = std::panic::catch_unwind(|| divide(a, b));
match result {
Ok(x) => x,
Err(_) => default,
}
}しかし、次の問題があるので避けたほうがよい。
-
パニックが常に巻き戻し(unwind)されるとは限らない。コンパイラのオプションや
Cargo.tomlのプロファイルで、パニック時にすぐプロセスを中止(abort)するよう設定できる。WebAssembly のように、設定によらず常に abort するビルドターゲットもある[profile.release] panic = 'abort' -
データ構造の処理中にパニックが起きると、データ構造が整合した状態に保たれる保証がなくなる。いわゆる例外安全性の問題で、Google が C++ で例外を禁じた理由の 1 つでもある(Google C++ Style Guide)
-
パニックの伝搬は FFI 境界と相性が悪い(項目34、参考)。Rust のコードで起きたパニックが、FFI 境界を越えて他の言語の呼び出し元に伝わらないよう、
catch_unwindで止める
代わりに、ライブラリのコードでは Result を返し、エラー処理を利用者に任せる。
panic を使ってよい場面
例外的に、次の場面ではパニックしてもよい。
main関数の中- エラーがきわめてまれで、利用者に毎回
unwrapを書かせたくない場合 - エラーの原因が不正な入力ではなく、内部データの破損である場合
不正な入力が想定内なら、入口となる関数を 2 つ用意するとよい。1 つは常に成功することを前提とし、失敗すればパニックするもの(例: String::from_utf8_unchecked)である。パニックすることは、API ガイドラインに従ってドキュメントの決まった節に書く(項目27)。もう 1 つは Result を返すもの(例: String::from_utf8)である。
panic を避けるための注意点
パニックは panic! 以外からも起きる。
unwrap、unwrap_err、expect、expect_errunreachable!- 範囲外のインデックスでのスライスの参照(
slice[index]) - 0 による除算(
x / y)
人間によるチェックでは不十分なので、CI でパニックしうるコードを検出する。たとえば no_panic クレートを使う。