パニックするかしないか

RustRustBeginner
オンラインで実践に進む

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のようなパニックする可能性のあるメソッドの呼び出しは、アプリケーションがエラーを処理する方法の置き換えとして意味されており、コードの残りの部分によって異なる場合があります。

同様に、unwrapexpectメソッドは、エラーをどのように処理するかを決定する準備ができる前のプロトタイピング時に非常に便利です。コードが堅牢になる準備ができたときに、コードに明確なマーカーを残します。

テストでメソッド呼び出しが失敗した場合、そのメソッドがテスト対象の機能でなくても、テスト全体が失敗することが望ましいです。panic!がテストが失敗としてマークされる方法であるため、unwrapまたはexpectを呼び出すことが正確に起こるべきことです。

コンパイラよりも多くの情報を持つ場合

ResultOk値を持つことを保証する他のロジックがあるが、そのロジックがコンパイラに理解されない場合、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ではなく特定の型を持っている場合、プログラムは「何か」を期待しています。そのため、コードはSomeNoneのバリアントの 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]。これは、呼び出し元のコードを書いているプログラマーに、修正する必要のあるバグがあることを知らせます。なぜなら、この範囲外のvalueGuessを作成すると、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関数を必ず使用する必要があります。それにより、GuessGuess::new関数の条件によってチェックされていないvalueを持つことがないことが保証されます。

その後、パラメータを持つか、または 1 から 100 の間の数字のみを返す関数は、そのシグネチャでi32ではなくGuessを受け取るか、または返すことを宣言でき、本体で追加のチェックを行う必要はありません。

まとめ

おめでとうございます!あなたは「パニックするかしないか」の実験を完了しました。あなたの技術を向上させるために、LabEx でさらに多くの実験を練習することができます。