panic! すべきか、すべきでないか
では、いつ panic! を呼び出し、いつ Result を返すべきかは、どのように判断すればよいのでしょうか? コードがパニックすると、そこから回復する方法はありません。回復可能かどうかにかかわらず、どんなエラー状況でも panic! を呼び出すことはできますが、そうすると、その状況は回復不能であるという判断を、呼び出し側のコードに代わって下すことになります。一方で Result 値を返すことを選べば、呼び出し側のコードに選択肢を与えることになります。呼び出し側のコードは、その状況に適した方法で回復を試みることもできますし、この場合の Err 値は回復不能だと判断して panic! を呼び出し、あなたの回復可能なエラーを回復不能なものに変えることもできます。したがって、失敗する可能性のある関数を定義するときのデフォルトとしては、Result を返すのがよい選択です。
しかし、サンプル、プロトタイプコード、テストのような状況では、Result を返すよりも、パニックするコードを書くほうが適切です。その理由を見ていき、その後で、コンパイラには失敗が不可能だとわからなくても、人間であるあなたにはわかる状況について議論しましょう。この章の最後では、ライブラリコードでパニックすべきかどうかを判断するための一般的な指針をいくつか示します。
サンプル、プロトタイプコード、テスト
ある概念を説明するためのサンプルを書くときに、堅牢なエラーハンドリングのコードまで含めると、サンプルがかえってわかりにくくなることがあります。サンプルでは、パニックする可能性のある unwrap のようなメソッド呼び出しは、アプリケーションでエラーをどのように処理したいかを後で埋めるためのプレースホルダーであると理解されます。その方法は、残りのコードが何をしているかによって異なりえます。
同様に、プロトタイピング中で、まだエラーをどう処理するか決める準備ができていないときには、unwrap メソッドと expect メソッドは非常に便利です。これらは、あとでプログラムをより堅牢にしようとするときのための明確な目印をコード内に残してくれます。
テスト内でメソッド呼び出しが失敗したなら、そのメソッド自体がテスト対象の機能でなくても、テスト全体が失敗してほしいはずです。そして、panic! はテストを失敗としてマークする方法なので、unwrap や expect を呼び出すのはまさに起こるべきことです。
コンパイラより多くの情報を持っている場合
また、Result が Ok 値を持つことを保証する別のロジックがあるものの、そのロジックをコンパイラが理解できない場合には、expect を呼び出すのも適切です。それでも、扱わなければならない Result 値は残ります。呼び出している操作は、あなたの特定の状況では論理的に失敗しえないとしても、一般論としては依然として失敗する可能性があるからです。コードを目で確認することで Err バリアントが決して発生しないと保証できるなら、expect を呼び出し、なぜ Err バリアントが起こらないと考えるのかを引数のテキストに記しておくのは、まったく問題ありません。例を見てみましょう。
fn main() {
use std::net::IpAddr;
let home: IpAddr = "127.0.0.1"
.parse()
.expect("Hardcoded IP address should be valid");
}
ここでは、ハードコードされた文字列をパースして IpAddr インスタンスを作成しています。127.0.0.1 が有効な IP アドレスであることはわかるので、ここで expect を使うのは問題ありません。しかし、有効な文字列をハードコードしているからといって、parse メソッドの戻り値の型が変わるわけではありません。私たちは依然として Result 値を受け取り、また、この文字列が常に有効な IP アドレスであることをコンパイラは見抜けるほど賢くないため、コンパイラは Err バリアントの可能性があるものとして Result を処理することを依然として要求します。もし IP アドレス文字列がプログラムにハードコードされているのではなくユーザーから渡されるものであり、そのため実際に失敗する可能性が ある のなら、Result はもっと堅牢な方法で処理したいはずです。この IP アドレスはハードコードされている、という前提を書いておけば、将来 IP アドレスを別のソースから取得する必要が出てきたときに、expect をより適切なエラーハンドリングコードへ変更するきっかけになります。
エラーハンドリングの指針
コードが不正な状態に陥る可能性がある場合には、コードをパニックさせるようにするのが望ましいです。この文脈でいう 不正な状態 とは、何らかの前提、保証、契約、または不変条件が破られている状態のことです。たとえば、無効な値、矛盾した値、欠けている値がコードに渡された場合などで、さらに次のうち 1 つ以上に当てはまる場合です。
- その不正な状態は、ユーザーが誤った形式でデータを入力する、といった時折起こりうるものではなく、想定外のものである。
- その時点以降のコードは、各段階で問題を確認し続けるのではなく、その不正な状態ではないことを前提にする必要がある。
- 使用している型の中にこの情報をうまくエンコードするよい方法がない。これがどういう意味かについては、第 18 章の 「状態と振る舞いを型としてエンコードする」 で例を通して見ていきます。
誰かがあなたのコードを呼び出して意味をなさない値を渡してきた場合には、可能であればエラーを返すのが最善です。そうすれば、ライブラリの利用者がその場合にどうしたいかを判断できます。しかし、処理を続けることが安全でなかったり有害であったりする場合には、panic! を呼び出して、あなたのライブラリを使っている人にそのコード内のバグを知らせ、開発中に修正してもらうのが最善の選択かもしれません。同様に、制御できない外部コードを呼び出していて、そのコードが修正しようのない不正な状態を返してきた場合にも、panic! はしばしば適切です。
しかし、失敗が予想される場合には、panic! を呼び出すよりも Result を返すほうが適切です。たとえば、パーサーに不正な形式のデータが渡される場合や、HTTP リクエストがレート制限に達したことを示すステータスを返す場合などです。このような場合に Result を返すことは、失敗が予期される可能性であり、それをどう扱うかは呼び出し側のコードが決めなければならないことを示します。
コードが、無効な値で呼び出されたときにユーザーを危険にさらすおそれのある操作を行う場合、そのコードはまず値が有効であることを検証し、値が無効ならパニックすべきです。これは主に安全性のためです。無効なデータに対して操作を試みると、コードが脆弱性にさらされる可能性があります。これが、範囲外のメモリアクセスを試みたときに標準ライブラリが panic! を呼び出す主な理由です。現在のデータ構造に属していないメモリへアクセスしようとすることは、一般的なセキュリティ上の問題だからです。関数にはしばしば 契約 があります。つまり、その振る舞いは、入力が特定の要件を満たしている場合にのみ保証されます。契約が破られたときにパニックするのは理にかなっています。なぜなら、契約違反は常に呼び出し側のバグを示しており、呼び出し側のコードに明示的な処理を強いたい種類のエラーではないからです。実際、呼び出し側のコードに回復のための妥当な方法はありません。コードを修正する必要があるのは、呼び出し側の プログラマ です。関数の契約、特に違反するとパニックを引き起こすような契約については、その関数の API ドキュメントで説明すべきです。
しかし、すべての関数に大量のエラーチェックを書くのは冗長で面倒です。幸いなことに、Rust の型システム(したがってコンパイラが行う型チェック)を使えば、多くのチェックを任せることができます。関数が特定の型を引数として受け取るなら、コンパイラがすでに有効な値であることを保証していると分かったうえで、そのコードのロジックを進められます。たとえば、Option ではなくある型を持っているなら、プログラムは 何か があることを期待しており、何もない ことを期待しているのではありません。そうすると、コードは Some バリアントと None バリアントの 2 つのケースを処理する必要がなくなります。確実に値がある 1 つのケースだけを扱えばよいのです。関数に何も渡そうとするコードは、そもそもコンパイルすら通りません。したがって、関数はそのケースを実行時にチェックする必要がありません。別の例として、u32 のような符号なし整数型を使えば、その引数が決して負にならないことが保証されます。
バリデーションのためのカスタム型
有効な値があることを Rust の型システムで保証するという考えを、もう一歩進めて、バリデーションのためのカスタム型を作成する方法を見てみましょう。第 2 章の数当てゲームを思い出してください。このコードでは、ユーザーに 1 から 100 までの数を予想させました。秘密の数と比較する前に、ユーザーの予想がその範囲内にあるかどうかは一度も検証しておらず、予想が正であることだけを検証していました。この場合、その結果はそれほど深刻ではありませんでした。出力が「大きすぎます」や「小さすぎます」であっても、依然として正しいからです。しかし、ユーザーを有効な予想へ導き、範囲外の数を予想した場合と、たとえば文字を入力した場合とで挙動を変えられるようにするのは、有用な改善になるでしょう。
これを行う 1 つの方法は、潜在的に負の数も許容するために、予想を単に u32 としてではなく i32 としてパースし、その後で数値が範囲内にあるかどうかのチェックを追加することです。次のようになります。
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
loop {
// --snip--
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
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--
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
この if 式は、値が範囲外かどうかをチェックし、問題をユーザーに伝え、continue を呼び出してループの次の反復を開始し、別の予想を求めます。if 式の後では、guess が 1 から 100 の間にあると分かったうえで、guess と秘密の数の比較を進められます。
しかし、これは理想的な解決策ではありません。プログラムが 1 から 100 までの値に対してのみ動作することが絶対的に重要で、しかもこの要件を持つ関数が多数あるなら、すべての関数でこのようなチェックを行うのは面倒ですし、性能にも影響するかもしれません。
代わりに、専用のモジュールで新しい型を作り、その型のインスタンスを生成する関数の中にバリデーションを入れることで、あらゆる場所で同じバリデーションを繰り返さずに済みます。そうすれば、関数はシグネチャで新しい型を安全に使え、受け取った値を安心して利用できます。リスト 9-13 は、new 関数が 1 から 100 までの値を受け取った場合にのみ Guess のインスタンスを生成する Guess 型の定義方法の一例を示しています。
#![allow(unused)]
fn main() {
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {value}.");
}
Guess { value }
}
pub fn value(&self) -> i32 {
self.value
}
}
}
なお、src/guessing_game.rs のこのコードは、ここでは示していない src/lib.rs へのモジュール宣言 mod guessing_game; の追加に依存しています。この新しいモジュールのファイル内では、i32 を保持する value という名前のフィールドを持つ Guess という名前の構造体を定義しています。数値はここに格納されます。
次に、Guess に対して Guess のインスタンスを生成する new という関連関数を実装します。new 関数は、i32 型の value という名前の引数を 1 つ受け取り、Guess を返すように定義されています。new 関数本体のコードは、value が 1 から 100 の間にあることを確認するために value を検査します。もし value がこの検査を通らなければ、panic! を呼び出します。これにより、呼び出し側のコードを書いているプログラマに、修正すべきバグがあることが伝わります。というのも、この範囲外の value で Guess を作成することは、Guess::new が前提としている契約に違反するからです。Guess::new が panic! する可能性のある条件は、その公開 API ドキュメントで説明されるべきです。panic! の可能性を API ドキュメントで示すための文書化の慣習については、第 14 章で扱います。value が検査を通った場合は、value フィールドを value 引数に設定した新しい Guess を作成し、その Guess を返します。
次に、self を借用し、ほかの引数を持たず、i32 を返す value という名前のメソッドを実装します。この種のメソッドは、フィールドからデータを取り出して返すことが目的であるため、ゲッター と呼ばれることがあります。この公開メソッドが必要なのは、Guess 構造体の value フィールドがプライベートだからです。value フィールドがプライベートであることは重要です。そうすることで、Guess 構造体を使うコードは value を直接設定できません。guessing_game モジュールの外側のコードは、Guess のインスタンスを作成するために 必ず Guess::new 関数を使わなければならず、その結果、Guess::new 関数の条件でチェックされていない value を Guess が持つ方法は存在しないことが保証されます。
その後、引数として受け取る、または返す値が 1 から 100 までの数だけである関数は、シグネチャで i32 ではなく Guess を受け取る、あるいは返すと宣言でき、関数本体で追加のチェックを行う必要がなくなります。
まとめ
Rust のエラーハンドリング機能は、より堅牢なコードを書けるように設計されています。panic! マクロは、プログラムが対処できない状態にあることを示し、無効または不正な値で処理を続けようとする代わりに、プロセスを停止するよう指示できるようにします。Result enum は Rust の型システムを使って、操作が失敗する可能性はあるが、その失敗はコードが回復できる種類のものであることを示します。Result を使えば、あなたのコードを呼び出すコードに対して、起こりうる成功または失敗を処理する必要があることを伝えられます。適切な状況で panic! と Result を使うことで、避けられない問題に直面しても、コードの信頼性を高められます。
標準ライブラリが Option と Result enum でジェネリクスをどのように有用に使っているかを見たので、次はジェネリクスがどのように機能するのか、そしてそれを自分のコードでどう使えるのかについて説明します。