はじめに
パニックするかしないかへようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習できます。
この実験では、panic!
を呼び出すか、Result
を返すかの判断は、エラー状況の回復可能性と呼び出しコードが利用できるオプションに依存します。
This tutorial is from open-source community. Access the source code
💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください
パニックするかしないかへようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習できます。
この実験では、panic!
を呼び出すか、Result
を返すかの判断は、エラー状況の回復可能性と呼び出しコードが利用できるオプションに依存します。
では、いつpanic!
を呼び出すべきか、いつResult
を返すべきかをどのように判断すればよいでしょうか。コードがパニックすると、回復する方法はありません。回復の可能性があるかどうかに関係なく、どんなエラー状況でもpanic!
を呼び出すことができますが、その場合、呼び出し元のコードにとって状況が回復不可能であるという判断を行ってしまいます。Result
値を返すことを選ぶと、呼び出し元のコードにオプションを与えることができます。呼び出し元のコードは、その状況に適した方法で回復を試みることができるかもしれませんし、この場合のErr
値が回復不可能であると判断して、panic!
を呼び出して、回復可能なエラーを回復不可能なエラーに変えることもできます。したがって、失敗する可能性のある関数を定義する際には、Result
を返すことが良い既定の選択肢です。
例、プロトタイプコード、テストなどの状況では、Result
を返す代わりにパニックするコードを書いた方が適切です。なぜそうなるのかを見てみましょう。その後、コンパイラが失敗が不可能であることを判断できないが、人間としては判断できる状況についても議論します。この章は、ライブラリコードでパニックするかどうかを決定するための一般的なガイドラインで締めくくります。
ある概念を説明するための例を書いているとき、堅牢なエラーハンドリングコードも含めると、例が分かりにくくなる場合があります。例では、unwrap
のようなパニックする可能性のあるメソッドの呼び出しは、アプリケーションがエラーを処理する方法の置き換えとして意味されており、コードの残りの部分によって異なる場合があります。
同様に、unwrap
とexpect
メソッドは、エラーをどのように処理するかを決定する準備ができる前のプロトタイピング時に非常に便利です。コードが堅牢になる準備ができたときに、コードに明確なマーカーを残します。
テストでメソッド呼び出しが失敗した場合、そのメソッドがテスト対象の機能でなくても、テスト全体が失敗することが望ましいです。panic!
がテストが失敗としてマークされる方法であるため、unwrap
またはexpect
を呼び出すことが正確に起こるべきことです。
Result
がOk
値を持つことを保証する他のロジックがあるが、そのロジックがコンパイラに理解されない場合、unwrap
またはexpect
を呼び出すのも適切です。まだ処理する必要のあるResult
値があります。呼び出している操作は一般的に失敗する可能性がありますが、特定の状況では論理的には不可能です。コードを手動で調べることでErr
バリアントが決して発生しないことを保証できる場合、unwrap
を呼び出すのは完全に許容され、expect
のテキストにErr
バリアントが決して発生しないと考える理由を記載するのがさらに良いです。以下は例です。
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
値を取得し、コンパイラはErr
バリアントがあり得るとしてResult
を処理するように強制されます。なぜなら、コンパイラはこの文字列が常に有効な IP アドレスであることを認識できないほど賢くないからです。IP アドレス文字列がユーザーからのものであり、プログラムにハードコードされていない場合、したがって失敗の可能性がある場合、もっと堅牢な方法でResult
を処理する必要があります。この IP アドレスがハードコードされているという仮定を述べることで、将来的に他のソースから IP アドレスを取得する必要がある場合に、expect
をより良いエラーハンドリングコードに変更するよう促されます。
コードが不適切な状態になる可能性がある場合、コードをパニックさせることが望ましいです。この文脈において、「不適切な状態」とは、いくつかの仮定、保証、契約、または不変条件が破られた場合です。たとえば、無効な値、矛盾する値、または欠損値がコードに渡された場合などです。さらに、以下の 1 つ以上が当てはまります。
誰かがコードを呼び出して、意味のない値を渡した場合、できる限りエラーを返す方が良いです。そうすることで、ライブラリのユーザーがその場合に何をしたいかを決定できます。ただし、続行することが不安定または有害な場合、最善の選択はpanic!
を呼び出して、ライブラリを使用している人にコードのバグを知らせることで、開発中に修正できるようにすることです。同様に、コントロールできない外部コードを呼び出して、修正できない無効な状態を返す場合、panic!
を使用するのが適切なことが多いです。
ただし、失敗が予想される場合、panic!
を呼び出すよりもResult
を返す方が適切です。例としては、不正な形式のデータが与えられたパーサーや、レート制限に達したことを示すステータスを返す HTTP リクエストなどが挙げられます。これらの場合、Result
を返すことは、失敗が予想される可能性であり、呼び出し元のコードがどのように処理するかを決定する必要があることを示しています。
コードが無効な値を使用して呼び出された場合にユーザーにリスクを与える操作を行う場合、コードはまず値が有効であることを検証し、値が無効な場合はパニックする必要があります。これは主にセキュリティ上の理由です。無効なデータで操作を試みると、コードが脆弱性にさらされる可能性があります。これが、標準ライブラリが境界外のメモリアクセスを試みた場合にpanic!
を呼び出す主な理由です。現在のデータ構造に属さないメモリをアクセスしようとすることは、一般的なセキュリティ問題です。関数には多くの場合「契約」があります。入力が特定の要件を満たす場合にのみ、その動作が保証されます。契約が破られたときにパニックするのは理にかなっています。なぜなら、契約の違反は常に呼び出し元側のバグを示しており、呼び出し元のコードが明示的に処理する必要のあるエラーの種類ではないからです。実際、呼び出し元のコードが回復する合理的な方法はありません。呼び出し元の「プログラマー」がコードを修正する必要があります。関数の契約、特に違反がパニックを引き起こす場合、関数の API ドキュメントに説明する必要があります。
ただし、すべての関数に多数のエラーチェックを行うと、冗長で面倒くさくなります。幸いなことに、Rust の型システム(したがってコンパイラによる型チェック)を使用して、多くのチェックを自動的に行うことができます。関数に特定の型のパラメータがある場合、コンパイラが既に有効な値を持っていることを確認しているので、コードのロジックを進めることができます。たとえば、Option
ではなく特定の型を持っている場合、プログラムは「何か」を期待しています。そのため、コードはSome
とNone
のバリアントの 2 つのケースを処理する必要がなくなります。値が必ずある 1 つのケースのみを持つことになります。関数に何も渡そうとするコードはコンパイルされません。したがって、関数は実行時にそのケースをチェックする必要がありません。もう 1 つの例は、u32
のような符号なし整数型を使用することです。これにより、パラメータが負にならないことが保証されます。
Rust の型システムを使って有効な値を保証するアイデアをさらに一歩進めて、検証用のカスタム型を作成してみましょう。第 2 章の当て推測ゲームを思い出してください。そのゲームでは、コードがユーザーに 1 から 100 の間の数字を当てるように依頼しました。秘密の数字と照合する前に、ユーザーの予想がそれらの数字の間にあることを検証したことはありませんでした。ただ、予想が正の数であることを検証しました。この場合、結果はそれほど深刻ではありませんでした。「数字が大きすぎます」または「数字が小さすぎます」という出力は依然として正しいです。しかし、ユーザーに有効な予想を導き、ユーザーが範囲外の数字を予想した場合と、例えば文字を入力した場合とで異なる動作をさせることは、便利な機能向上になります。
これを行う 1 つの方法は、潜在的に負の数を許可するために、u32
ではなくi32
として予想を解析することであり、その後、数字が範囲内であることをチェックすることです。次のようになります。
ファイル名:src/main.rs
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-13 は、new
関数が 1 から 100 の間の値を受け取った場合にのみGuess
のインスタンスを作成するGuess
型を定義する 1 つの方法を示しています。
ファイル名:src/lib.rs
1 pub struct Guess {
value: i32,
}
impl Guess {
2 pub fn new(value: i32) -> Guess {
3 if value < 1 || value > 100 {
4 panic!(
"Guess value must be between 1 and 100, got {}.",
value
);
}
5 Guess { value }
}
6 pub fn value(&self) -> i32 {
self.value
}
}
リスト 9-13: 1 から 100 の間の値のみで動作するGuess
型
まず、i32
を保持するvalue
というフィールドを持つGuess
という名前の構造体を定義します [1]。ここに数字が格納されます。
次に、Guess
に対してGuess
値のインスタンスを作成するnew
という関連付け関数を実装します [2]。new
関数は、i32
型のvalue
という 1 つのパラメータを持ち、Guess
を返すように定義されています。new
関数の本体のコードは、value
が 1 から 100 の間であることを確認するためにvalue
をテストします [3]。value
がこのテストに合格しない場合、panic!
を呼び出します [4]。これは、呼び出し元のコードを書いているプログラマーに、修正する必要のあるバグがあることを知らせます。なぜなら、この範囲外のvalue
でGuess
を作成すると、Guess::new
が依存している契約に違反するからです。Guess::new
がパニックする可能性のある条件については、その公開 API ドキュメントで議論する必要があります。第 14 章で作成する API ドキュメントにおいてpanic!
の可能性を示すドキュメント作成規約についても説明します。value
がテストに合格した場合、value
フィールドをvalue
パラメータに設定した新しいGuess
を作成し、Guess
を返します [5]。
次に、self
を借用し、他のパラメータを持たず、i32
を返すvalue
というメソッドを実装します [6]。この種のメソッドは、時には「ゲッター」と呼ばれます。なぜなら、その目的はフィールドからいくつかのデータを取得して返すことだからです。この公開メソッドは必要です。なぜなら、Guess
構造体のvalue
フィールドは非公開だからです。value
フィールドが非公開であることが重要です。なぜなら、Guess
構造体を使用するコードは、value
を直接設定することが許可されないからです。モジュール外のコードは、Guess
のインスタンスを作成するためにGuess::new
関数を必ず使用する必要があります。それにより、Guess
がGuess::new
関数の条件によってチェックされていないvalue
を持つことがないことが保証されます。
その後、パラメータを持つか、または 1 から 100 の間の数字のみを返す関数は、そのシグネチャでi32
ではなくGuess
を受け取るか、または返すことを宣言でき、本体で追加のチェックを行う必要はありません。
おめでとうございます!あなたは「パニックするかしないか」の実験を完了しました。あなたの技術を向上させるために、LabEx でさらに多くの実験を練習することができます。