panic!
すべきかするまいか
では、panic!
すべき時とResult
を返すべき時はどう決定すればいいのでしょうか?コードがパニックしたら、
回復する手段はありません。回復する可能性のある手段の有る無しに関わらず、どんなエラー場面でもpanic!
を呼ぶことはできますが、
そうすると、呼び出す側のコードの立場に立ってこの場面は回復不能だという決定を下すことになります。
Result
値を返す決定をすると、決断を下すのではなく、呼び出し側に選択肢を与えることになります。
呼び出し側は、場面に合わせて回復を試みることを決定したり、この場合のErr
値は回復不能と断定して、
panic!
を呼び出し、回復可能だったエラーを回復不能に変換することもできます。故に、Result
を返却することは、
失敗する可能性のある関数を定義する際には、いい第一選択肢になります。
稀な場面では、Result
を返すよりもパニックするコードを書く方がより適切になることもあります。
例やプロトタイプコード、テストでパニックするのが適切な理由を探ってみましょう。
それからコンパイラではありえない失敗だと気づけなくとも、人間なら気づける場面を議論しましょう。
そして、ライブラリコードでパニックするか決定する方法についての一般的なガイドラインで結論づけましょう。
例、プロトタイプコード、テスト
例を記述して何らかの概念を具体化している時、頑健なエラー処理コードも例に含むことは、例の明瞭さを欠くことになりかねません。
例において、unwrap
などのパニックする可能性のあるメソッド呼び出しは、
アプリケーションにエラーを処理してほしい方法へのプレースホルダーを意味していると理解され、
これは残りのコードがしていることによって異なる可能性があります。
同様に、unwrap
やexpect
メソッドは、エラーの処理法を決定する準備ができる前、プロトタイプの段階では、
非常に便利です。それらにより、コードにプログラムをより頑健にする時の明らかなマーカーが残されるわけです。
メソッド呼び出しがテスト内で失敗したら、そのメソッドがテスト下に置かれた機能ではなかったとしても、
テスト全体が失敗してほしいでしょう。panic!
が、テストが失敗と印づけられる手段なので、
unwrap
やexpect
の呼び出しはズバリ起こるべきことです。
コンパイラよりもプログラマがより情報を持っている場合
Result
がOk
値であると確認する何らかの別のロジックがある場合、unwrap
を呼び出すことは適切でしょうが、
コンパイラは、そのロジックを理解はしません。それでも、処理する必要のあるResult
は存在するでしょう:
呼び出している処理が何であれ、自分の特定の場面では論理的に起こり得なくても、一般的にまだ失敗する可能性はあるわけです。
手動でコードを調査してErr
列挙子は存在しないと確認できたら、unwrap
を呼び出すことは完全に受容できることです。
こちらが例です:
#![allow(unused)] fn main() { use std::net::IpAddr; let home: IpAddr = "127.0.0.1".parse().unwrap(); }
ハードコードされた文字列を構文解析することでIpAddr
インスタンスを生成しています。
プログラマには127.0.0.1
が合法なIPアドレスであることがわかるので、ここでunwrap
を使用することは、
受容可能なことです。しかしながら、ハードコードされた合法な文字列が存在することは、
parse
メソッドの戻り値型を変えることにはなりません: それでも得られるのは、Result
値であり、
コンパイラはまだErr
列挙子になる可能性があるかのようにResult
を処理することを強制してきます。
コンパイラは、この文字列が常に合法なIPアドレスであると把握できるほど利口ではないからです。
プログラムにハードコードされるのではなく、IPアドレス文字列がユーザ起源でそれ故に確かに失敗する可能性がある場合、
Result
をもっと頑健な方法で処理したほうが絶対にいいでしょう。
エラー処理のガイドライン
コードが悪い状態に陥る可能性があるときにパニックさせるのは、推奨されることです。この文脈において、 悪い状態とは、何らかの前提、保証、契約、不変性が破られたことを言い、例を挙げれば、無効な値、 矛盾する値、行方不明な値がコードに渡されることと、さらに以下のいずれか一つ以上の状態であります:
- 悪い状態がときに起こるとは予想されないとき。
- この時点以降、この悪い状態にないことを頼りにコードが書かれているとき。
- 使用している型にこの情報をコード化するいい手段がないとき。
誰かが自分のコードを呼び出して筋の通らない値を渡してきたら、最善の選択肢はpanic!
し、
開発段階で修正できるように自分たちのコードにバグがあることをライブラリ使用者に通知することかもしれません。
同様に自分の制御下にない外部コードを呼び出し、修正しようのない無効な状態を返すときにpanic!
はしばしば適切です。
しかし、どんなにコードをうまく書いても起こると予想されますが、悪い状態に達したとき、それでもpanic!
呼び出しをするよりも、
Result
を返すほうがより適切です。例には、不正なデータを渡されたパーサとか、
訪問制限に引っかかったことを示唆するステータスを返すHTTPリクエストなどが挙げられます。
このような場合には、呼び出し側が問題の処理方法を決定できるようにResult
を返してこの悪い状態を委譲して、
失敗が予想される可能性であることを示唆するべきです。panic!
を呼び出すことは、
これらのケースでは最善策ではないでしょう。
コードが値に対して処理を行う場合、コードはまず値が合法であることを確認し、
値が合法でなければパニックするべきです。これはほぼ安全性上の理由によるものです: 不正なデータの処理を試みると、
コードを脆弱性に晒す可能性があります。これが、境界外へのメモリアクセスを試みたときに標準ライブラリがpanic!
を呼び出す主な理由です:
現在のデータ構造に属しないメモリにアクセスを試みることは、ありふれたセキュリティ問題なのです。
関数にはしばしば契約が伴います: 入力が特定の条件を満たすときのみ、振る舞いが保証されるのです。
契約が侵されたときにパニックすることは、道理が通っています。なぜなら、契約侵害は常に呼び出し側のバグを示唆し、
呼び出し側に明示的に処理してもらう必要のある種類のエラーではないからです。実際に、
呼び出し側が回復する合理的な手段はありません; 呼び出し側のプログラマがコードを修正する必要があるのです。
関数の契約は、特に侵害がパニックを引き起こす際には、関数のAPIドキュメント内で説明されているべきです。
ですが、全ての関数でたくさんのエラーチェックを行うことは冗長で煩わしいことでしょう。幸運にも、
Rustの型システム(故にコンパイラが行う型精査)を使用して多くの検査を行ってもらうことができます。
関数の引数に特定の型があるなら、合法な値があるとコンパイラがすでに確認していることを把握して、
コードのロジックに進むことができます。例えば、Option
以外の型がある場合、プログラムは、
何もないではなく何かあると想定します。そうしたらコードは、
Some
とNone
列挙子の2つの場合を処理する必要がなくなるわけです:
確実に値があるという可能性しかありません。関数に何もないことを渡そうとしてくるコードは、
コンパイルが通りもしませんので、その場合を実行時に検査する必要はないわけです。
別の例は、u32
のような符号なし整数を使うことであり、この場合、引数は負には絶対にならないことが確認されます。
検証のために独自の型を作る
Rustの型システムを使用して合法な値があると確認するというアイディアを一歩先に進め、 検証のために独自の型を作ることに目を向けましょう。第2章の数当てゲームで、 コードがユーザに1から100までの数字を推測するよう求めたことを思い出してください。 秘密の数字と照合する前にユーザの推測がそれらの値の範囲にあることを全く確認しませんでした; 推測が正であることしか確認しませんでした。この場合、結果はそれほど悲惨なものではありませんでした: 「大きすぎ」、「小さすぎ」という出力は、それでも正しかったでしょう。ユーザを合法な推測に導き、 ユーザが範囲外の数字を推測したり、例えばユーザが文字を代わりに入力したりしたときに別の挙動をするようにしたら、 有益な改善になるでしょう。
これをする一つの方法は、ただのu32
の代わりにi32
として推測をパースし、負の数になる可能性を許可し、
それから数字が範囲に収まっているというチェックを追加することでしょう。そう、以下のように:
loop {
// --snip--
let guess: i32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
if guess < 1 || guess > 100 {
println!("The secret number will be between 1 and 100.");
continue;
}
match guess.cmp(&secret_number) {
// --snip--
}
このif
式が、値が範囲外かどうかをチェックし、ユーザに問題を告知し、continue
を呼び出してループの次の繰り返しを始め、
別の推測を求めます。if
式の後、guess
は1から100の範囲にあると把握して、guess
と秘密の数字の比較に進むことができます。
ところが、これは理想的な解決策ではありません: プログラムが1から100の範囲の値しか処理しないことが間違いなく、 肝要であり、この要求がある関数の数が多ければ、このようなチェックを全関数で行うことは、 面倒でパフォーマンスにも影響を及ぼす可能性があるでしょう。
代わりに、新しい型を作って検証を関数内に閉じ込め、検証を全箇所で繰り返すのではなく、
その型のインスタンスを生成することができます。そうすれば、関数がその新しい型をシグニチャに用い、
受け取った値を自信を持って使用することは安全になります。リスト9-9に、new
関数が1から100までの値を受け取った時のみ、
Guess
のインスタンスを生成するGuess
型を定義する一つの方法を示しました。
#![allow(unused)] fn main() { pub struct Guess { value: u32, } impl Guess { pub fn new(value: u32) -> Guess { if value < 1 || value > 100 { // 予想の値は1から100の範囲でなければなりませんが、{}でした panic!("Guess value must be between 1 and 100, got {}.", value); } Guess { value } } pub fn value(&self) -> u32 { self.value } } }
まず、u32
型のvalue
をフィールドに持つGuess
という名前の構造体を定義しています。
ここに数値が保管されます。
それからGuess
にGuess
値のインスタンスを生成するnew
という名前の関連関数を実装しています。
new
関数は、u32
型のvalue
という引数を取り、Guess
を返すように定義されています。
new
関数の本体のコードは、value
をふるいにかけ、1から100の範囲であることを確かめます。
value
がふるいに引っかかったら、panic!
呼び出しを行います。これにより、呼び出しコードを書いているプログラマに、
修正すべきバグがあると警告します。というのも、この範囲外のvalue
でGuess
を生成することは、
Guess::new
が頼りにしている契約を侵害するからです。Guess::new
がパニックするかもしれない条件は、
公開されているAPIドキュメントで議論されるべきでしょう; あなたが作成するAPIドキュメントでpanic!
の可能性を示唆する、
ドキュメントの規約は、第14章で講義します。value
が確かにふるいを通ったら、
value
フィールドがvalue
引数にセットされた新しいGuess
を作成して返します。
次に、self
を借用し、他に引数はなく、u32
を返すvalue
というメソッドを実装します。
この類のメソッドは時にゲッターと呼ばれます。目的がフィールドから何らかのデータを得て返すことだからです。
この公開メソッドは、Guess
構造体のvalue
フィールドが非公開なので、必要になります。
value
フィールドが非公開なことは重要であり、そのためにGuess
構造体を使用するコードは、
直接value
をセットすることが叶わないのです: モジュール外のコードは、
Guess::new
関数を使用してGuess
のインスタンスを生成しなければならず、
それにより、Guess::new
関数の条件式でチェックされていないvalue
がGuess
に存在する手段はないことが保証されるわけです。
そうしたら、引数を一つ持つか、1から100の範囲の数値のみを返す関数は、シグニチャでu32
ではなく、
Guess
を取るか返し、本体内で追加の確認を行う必要はなくなると宣言できるでしょう。
まとめ
Rustのエラー処理機能は、プログラマがより頑健なコードを書く手助けをするように設計されています。
panic!
マクロは、プログラムが処理できない状態にあり、無効だったり不正な値で処理を継続するのではなく、
プロセスに処理を中止するよう指示することを通知します。Result
enumは、Rustの型システムを使用して、
コードが回復可能な方法で処理が失敗するかもしれないことを示唆します。Result
を使用して、
呼び出し側のコードに成功や失敗する可能性を処理する必要があることも教えます。
適切な場面でpanic!
やResult
を使用することで、必然的な問題の眼前でコードの信頼性を上げてくれます。
今や、標準ライブラリがOption
やResult
enumなどでジェネリクスを有効活用するところを目の当たりにしたので、
ジェネリクスの動作法と自分のコードでの使用方法について語りましょう。