Skip to content

Latest commit

 

History

History
536 lines (463 loc) · 25.1 KB

File metadata and controls

536 lines (463 loc) · 25.1 KB

9. Error Handling(エラーハンドリング)

  • コードがコンパイルされる前にエラーの可能性を認識してくれる
  • エラーを、recoverableunrecoverableなエラーが主要なカテゴリ
    • recoverable:ファイルが見つからないエラーなど、操作を再試行する問題がなくなる状態
    • unrecoverable:配列の末尾を超えた場所にアクセスしようとした場合など、バグのある状態
    • ほとんどの言語ではこの二つを区別していない
  • Rustには例外がない。その代わりとして、二種類用意されている
    • recoverableResult<T, E>
    • unrecoverable:実行を停止するpanic!

panic!unrecoverableなエラー

  • panic!マクロ
    • 実行されると、プログラムは失敗メッセージを表示し、スタックを巻き戻してクリーンアップした後、終了する
    • バグが検出された場合に最も一般的に発生するもの
パニックに対応したスタックの巻き戻しや中止
  • パニック発生時のデフォルト対応
    • プログラムは巻き戻しを開始し、スタックに戻って各関数からデータをクリーンアップする
  • 別の方法として、クリーンアップを行わずにプログラムを終了させることができる
    • プログラムが使用していたメモリは、オペレーティングシステムによってクリーンアップされる必要がある
    • 結果として得られるバイナリをできるだけ小さくする必要がある場合は、Cargo.tomlファイルの適切な[profile]セクションにpanic = 'abort'を追加
      • パニックが発生したときに巻き戻しから中止に切り替えられる
  • ex) リリースモードでパニック時に中止したい場合は下記のように記述
[profile.release]
panic = 'abort'
  • panic!を呼び出してみる
fn main() {
    panic!("crash and burn");
}
panic!を使ってバックトレース
  • ベクターの終端を超えてアクセスするとpanic!が呼ばれる
    • 向こうなインデックスを渡した場合、返す要素がない → パニックになる
fn main() {
    let v = vec![1, 2, 3];

    v[99];
}
  • C言語
    • データ構造体の終端を越えて読み込もうとすると、未定義の動作になる
    • メモリがその構造体に属していなくても、データ構造体のその要素に対応するメモリの位置にあるものは何でも取得できるかもしれない
      • バッファオーバーリードと呼ばれ、攻撃者がデータ構造体の後に格納されている許可されていないデータを読み取るような方法でインデックスを操作することができた場合、セキュリティ上の脆弱性につながる可能性がある
        • この脆弱性からプログラムを守るため、Rustは実行を停止して続行を拒否する
$ cargo run
   Compiling panic v0.1.0 (file:///projects/panic)
    Finished dev [unoptimized + debuginfo] target(s) in 0.27s
     Running `target/debug/panic`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', /rustc/5e1a799842ba6ed4a57e91f7ab9435947482f7d8/src/libcore/slice/mod.rs:2806:10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
  • Rustのsliceの実装

    • このエラーは libcore/slice/mod.rsという書き込んでいないファイルを指している
    • ベクトルv[]を使用したときに実行されるコードはlibcore/slice/mod.rsにあり、panic!が実際に起こっている箇所
  • 環境変数RUST_BACKTRACEを設定して、エラーの原因となったことのバックトレースを取得できる

    • バックトレースは、この時点に至るまでに呼び出されたすべての関数のリスト
  • 環境変数RUST_BACKTRACEが設定されているときにpanic!の呼び出しによって生成されたバックトレース

$ RUST_BACKTRACE=1 cargo run
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', /rustc/5e1a799842ba6ed4a57e91f7ab9435947482f7d8/src/libcore/slice/mod.rs:2806:10
stack backtrace:
   0: backtrace::backtrace::libunwind::trace
             at /Users/runner/.cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.40/src/backtrace/libunwind.rs:88
   1: backtrace::backtrace::trace_unsynchronized
             at /Users/runner/.cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.40/src/backtrace/mod.rs:66
   2: std::sys_common::backtrace::_print_fmt
             at src/libstd/sys_common/backtrace.rs:84
   3: <std::sys_common::backtrace::_print::DisplayBacktrace as core::fmt::Display>::fmt
             at src/libstd/sys_common/backtrace.rs:61
   4: core::fmt::ArgumentV1::show_usize
   5: std::io::Write::write_fmt
             at src/libstd/io/mod.rs:1426
   6: std::sys_common::backtrace::_print
             at src/libstd/sys_common/backtrace.rs:65
   7: std::sys_common::backtrace::print
             at src/libstd/sys_common/backtrace.rs:50
   8: std::panicking::default_hook::{{closure}}
             at src/libstd/panicking.rs:193
   9: std::panicking::default_hook
             at src/libstd/panicking.rs:210
  10: std::panicking::rust_panic_with_hook
             at src/libstd/panicking.rs:471
  11: rust_begin_unwind
             at src/libstd/panicking.rs:375
  12: core::panicking::panic_fmt
             at src/libcore/panicking.rs:84
  13: core::panicking::panic_bounds_check
             at src/libcore/panicking.rs:62
  14: <usize as core::slice::SliceIndex<[T]>>::index
             at /rustc/5e1a799842ba6ed4a57e91f7ab9435947482f7d8/src/libcore/slice/mod.rs:2806
  15: core::slice::<impl core::ops::index::Index<I> for [T]>::index
             at /rustc/5e1a799842ba6ed4a57e91f7ab9435947482f7d8/src/libcore/slice/mod.rs:2657
  16: <alloc::vec::Vec<T> as core::ops::index::Index<I>>::index
             at /rustc/5e1a799842ba6ed4a57e91f7ab9435947482f7d8/src/liballoc/vec.rs:1871
  17: panic::main
             at src/main.rs:4
  18: std::rt::lang_start::{{closure}}
             at /rustc/5e1a799842ba6ed4a57e91f7ab9435947482f7d8/src/libstd/rt.rs:67
  19: std::rt::lang_start_internal::{{closure}}
             at src/libstd/rt.rs:52
  20: std::panicking::try::do_call
             at src/libstd/panicking.rs:292
  21: __rust_maybe_catch_panic
             at src/libpanic_unwind/lib.rs:78
  22: std::panicking::try
             at src/libstd/panicking.rs:270
  23: std::panic::catch_unwind
             at src/libstd/panic.rs:394
  24: std::rt::lang_start_internal
             at src/libstd/rt.rs:51
  25: std::rt::lang_start
             at /rustc/5e1a799842ba6ed4a57e91f7ab9435947482f7d8/src/libstd/rt.rs:67
  26: panic::main
  • バックトレースを取得するために、デバッグシンボルを有効にする必要がある
    • デバッグシンボルはデフォルトで有効になっている
    • --releaseをつけずにcargo runもしくはcargo buildを使う場合、デバッグシンボルは有効になる

Recoverable エラーとResult

  • ほとんどのエラーは、プログラムを完全に停止させるほど深刻なものではありません

    • 失敗したとき、それはあなたが簡単に解釈して対応できるような理由であることもある
      • ex) ファイルを開こうとして、その操作がファイルが存在しないために失敗した場合、プロセスを終了させるのではなく、ファイルを作成したいと思うかもしれない
  • OkErrの2つを定義されている列挙型

    • TEは汎用型パラメータ
    • TOk内で成功した場合に返される値の型を表す
    • EErr内で失敗した場合に返されるエラーの型を表す
enum Result<T, E> {
    Ok(T),
    Err(E),
}
  • 関数が失敗する可能性があるので、Result値を返す関数を呼び出す
    • ex) ファイルを開く
use std::fs::File;

fn main() {
    let f = File::open("hello.txt");
}
  • File::openResultを返すことをどうやって知るのか?
  • let flet f: u32に変更してcargo run
    • File::open関数の戻り値の型がResult<T, E>
$ cargo run
   Compiling error_handling v0.1.0 (file://error_handling)
error[E0308]: mismatched types
 --> src/main.rs:5:18
  |
5 |     let f: u32 = File::open("hello.txt");
  |            ---   ^^^^^^^^^^^^^^^^^^^^^^^ expected `u32`, found enum `std::result::Result`
  |            |
  |            expected due to this
  |
  = note: expected type `u32`
             found enum `std::result::Result<std::fs::File, std::io::Error>`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
error: could not compile `error_handling`.

To learn more, run the command again with --verbose.
  • File::openが返す値に応じて異なるアクションを実行する
    • Result enumなどはスコープに入っているので、OkErrResult::を指定する必要がない
    • 結果がOkの場合、内部のファイル値を返す
    • もう一方のアームは、File::openからErr値を取得した場合を処理
      • ex) panic!マクロを呼び出す
use std::fs::File;

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => panic!("Problem opening the file: {:?}", error),
    };
}
  • 実行すると、panic!が呼び出される
$ cargo run
   Compiling error_handling v0.1.0 (file://error_handling)
warning: unused variable: `f`
 --> src/main.rs:6:9
  |
6 |     let f = match f {
  |         ^ help: consider prefixing with an underscore: `_f`
  |
  = note: `#[warn(unused_variables)]` on by default

    Finished dev [unoptimized + debuginfo] target(s) in 0.22s
     Running `target/debug/error_handling`
thread 'main' panicked at 'Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:8:23
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
異なるエラーでのマッチング
  • ファイルが存在せずFile::openが失敗した場合、ファイルを作成して新しいファイルのハンドルを返す
    • ファイルを開く権限がなかったなどの理由でFile::openが失敗した場合、panic!内部のマッチ式を追加する
  • 異なる種類のエラーを異なる方法で処理する
    • File::openErr内で返す値の型は、標準ライブラリが提供する構造体であるio::Error
      • io::ErrorKind値を取得するために呼び出すことができるメソッドの種類がある
      • enum io::ErrorKindは標準ライブラリで提供
      • ErrorKind::NotFoundで、開こうとしているファイルがまだ存在しないことを示す
        • error.kind()でもマッチする
    • File::createでファイルを作成
      • File::createも失敗する可能性があるので、内側のマッチ式に二つ目のアームが必要
      • ファイルを作成できない場合は、別のエラーメッセージが表示
      • ファイルが作成できない以外のエラーが発生してもプログラムはpanic!になる
use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Problem creating the file: {:?}", e),
            },
            other_error => {
                panic!("Problem opening the file: {:?}", other_error)
            }
        },
    };
}
use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = File::open("hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("Problem creating the file: {:?}", error);
            })
        } else {
            panic!("Problem opening the file: {:?}", error);
        }
    });
}
エラー時にパニックになるためのショートカット:unwrapexpect
  • Result<T, E>型には、さまざまなタスクを実行するために定義された多くのヘルパーメソッドがあある
  • unwrap
    • Okの場合、Okの内部の値を返す
    • Errの場合、panic!マクロを呼び出す
use std::fs::File;

fn main() {
    let f = File::open("hello.txt").unwrap();
}
  • expect
    • panic!エラーメッセージを選択することもできる
    • 良いエラーメッセージを提供することで、意図を伝わり、原因を追求しやすくなる
use std::fs::File;

fn main() {
    let f = File::open("hello.txt").expect("Failed to open hello.txt");
}
エラーの伝搬
  • エラーを伝搬する
    • 実装が何かを呼び出して失敗する可能性のある関数を書いている場合、その関数内でエラーを処理する代わりに、呼び出し元のコードにエラーを返して、呼び出し元のコードが何をすべきかを判断できるようにする
    • コードのコンテキストで利用できるものよりもエラーがどのように処理されるべきかを指示する情報もしくはロジックがある場合、呼び出し側のコードにより多くのコントロールを与える
    • ex) ファイルが存在しないか読み込めない場合、この関数はこの関数を呼び出したコードにこれらのエラーを返す
      • 関数の戻り値の型:Result<String, io::Error>
        • Result<T, E>型の値を返していることを意味している
        • 成功した場合、Stringを保持するOk値を受け取る
        • 問題が発生した場合、問題のある詳細な情報を含むio::Errorのインスタンスを保持するErr値を受け取る
          • 戻り値の型としてio::Errorを選択
            • File::open関数とread_to_stringメソッドの両方で失敗する可能性のある操作から返されるエラー値の型
use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
    let f = File::open("hello.txt");

    let mut f = match f {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut s = String::new();

    match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        Err(e) => Err(e),
    }
}
エラーを伝播するためのショートカット:?演算子
  • 先ほどのread_username_from_file?演算子を用いる
    • ? Resultの値がOkの場合、Okの内側から値を返される
    • Errの場合、関数全体から返される(エラー値は呼び出しコードに伝搬される)
use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
    let mut f = File::open("hello.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}
  • ?演算子を使用することで、多くのボイラプレートが削除され、実装がよりシンプルになる
use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
    let mut s = String::new();

    File::open("hello.txt")?.read_to_string(&mut s)?;

    Ok(s)
}
  • さらに短くする
    • 「ファイルを文字列に読み込む」
use std::fs;
use std::io;

fn read_username_from_file() -> Result<String, io::Error> {
    fs::read_to_string("hello.txt")
}
Resultを返す関数で使用できる?演算子
  • ?演算子
    • Resultのリターン型を持つ関数で使うことができる
    • マッチのうちResultの戻り値の型を必要とする部分はreturn Err(e)なので、関数の戻り値の型は互換性のあるResultにできる
use std::fs::File;

fn main() {
    let f = File::open("hello.txt")?;
}
  • 実行してみる
    • ResultOption、またはstd::ops::Tryを実装した別の型を返す関数でしか使えないとエラーが出る
$ cargo run
   Compiling error-handling v0.1.0 (file://error-handling)
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `std::ops::Try`)
 --> src/main.rs:4:13
  |
3 | / fn main() {
4 | |     let f = File::open("hello.txt")?;
  | |             ^^^^^^^^^^^^^^^^^^^^^^^^ cannot use the `?` operator in a function that returns `()`
5 | | }
  | |_- this function should return `Result` or `Option` to accept `?`
  |
  = help: the trait `std::ops::Try` is not implemented for `()`
  = note: required by `std::ops::Try::from_error`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling`.

To learn more, run the command again with --verbose.
  • Result<T, E>を返す他の関数を呼び出すときに?を使いたい場合
    • 関数の戻り値の型を Result<T, E>に変更する
    • matchまたはResult<T, E>メソッドの一つを使用して、Result<T, E>を処理すること
  • Box<dyn Error>型はtraitオブジェクト
use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
    let f = File::open("hello.txt")?;

    Ok(())
}

panic!になるか、ならないか

  • panic!を呼ぶときや、いつResultを返すべきかの判断は?
    • panic!に陥った場合、復旧する方法はない
    • Resultの値を返すことを選択した場合、呼び出し元のコードに決定を下すのではなく、呼び出し元のコードに選択肢を与える
      • 呼び出したコードは、その状況に適した方法で回復を試みることを選択するか、この場合のErr値は回復不可能であると判断し、panic!を呼び出して、回復可能なエラーを回復不可能なものに変えることができる
    • 失敗する可能性のある関数を定義している場合はResultを返すのが良いデフォルトの選択
  • Resultを返す代わりにpanic!を起こすコードを書くこともある
例、プロトタイプコード、テスト
  • ある概念を説明するために例を書いているとき、例の中に堅牢なエラー処理コードを持つことは、例をあまり明確にしないことがある
    • panic!を起こす可能性のあるunwrapメソッドへの呼び出しは、アプリケーションがエラーを処理する方法のプレースホルダとして意図されていると理解されている
  • unwrapメソッドとexpectメソッド
    • エラー処理の方法を決める前にプロトタイピングをするときに非常に便利
    • プログラムをより堅牢なものにするための明確なマーカーをコードに残してくれる
  • テストでメソッドの呼び出しが失敗した場合、そのメソッドがテスト対象の機能でなくても、テスト全体を失敗させたいと考える
    • panic!はテストが失敗としてマークされる方法なので、unwrapexpectを呼び出すことはあるべき姿
コンパイラよりも多くの情報を持っているケース
  • ResultOk値を持つロジックがある場合にunwrapを呼び出すのも適切ですが、そのロジックはコンパイラが理解できない
    • コードを手動で検査することで、Errを決して持たないことを保証できるのであれば、unwrapを呼び出しても全く問題ない
  • ハードコードされた文字列を解析してIpAddrのインスタンスを作成
    • 127.0.0.0.1が有効なIPアドレスである
    • ハードコードされた文字列を扱っても、parseメソッドの戻り値は変わらない
    • 文字列がユーザからのものであり、失敗の可能性があるのであれば、Resultをより堅牢な方法で処理したい
use std::net::IpAddr;

let home: IpAddr = "127.0.0.1".parse().unwrap();
エラー処理のガイドライン
  • コードがバッドステートになる可能性がある場合は、コードをパニック状態にすることをお勧めする

  • バッドステートとは、何らかの仮定、保証、契約、不変性が破られた状態のことを指す

    • バッドステートは、たまに起こることを想定したものではない
    • ある時点以降のコードは、バッドステートにならないこと
    • 利用タイプの情報でエンコードするのはあまり良い方法ではない
  • 間違った値を渡してコードを呼び出した場合、panic!でライブラリを利用している人にバグを警告してあげる

  • 想定される失敗がある場合は、panic!コールよりもResultを返す方が適切

    • ex) パーサーに不正なデータが与えられたり、HTTPリクエストがレート制限に達したことを示すステータスを返したりするとき、Resultを返す
  • コードが値に対して操作を行う場合、最初に値が有効であることを確認し、有効でない場合にパニックを起こすべき

    • 主に安全上の理由から
      • 無効なデータを操作しようとするとコードが脆弱性にさらされる可能性がある
      • 範囲外のメモリアクセスを試みた場合に標準ライブラリがパニックを呼ぶ主な理由でもある
        • データ構造に属さないメモリにアクセスしようとすることは、一般的なセキュリティ問題
      • 関数は入力が特定の要件を満たしている場合にのみ、その動作が保証される
        • 違反は呼び出し側のバグを示し、呼び出し側のコードが明示的に処理しなければならないような種類のエラーではないから - 違反がパニックを引き起こす場合は、その関数のAPIドキュメントで説明されるべき
  • すべての関数にたくさんのエラーチェックがあると、冗長で煩わしいものになる

  • Rustの型システム(コンパイラが行う型チェック)を使用することで、多くのチェックを行える

    • 関数のパラメータに特定の型が指定されている場合、コンパイラが有効な値を確認していることを知っていれば、コードのロジックを進めることができる
      • ex1) Optionではなく型を指定した場合、プログラムは何もないよりも何かがあることを期待する
        • コードはSomeNoneの2つのケースを処理する必要はなく、関数に何も渡そうとするコードはコンパイルすらしないので、関数は実行時にそのケースをチェックする必要がない
      • ex2) u32のような符号なし整数型を使用することで、パラメータが負の値になることはない
バリデーション用のカスタム型の作成
  • Rustの型システムを使用して有効な値を確保するという考えの検証用のカスタム型を作成
  • 推測を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--
    }
  • 新しい型を作成して、その型のインスタンスを作成するための関数にバリデーションを入れておく
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
    }
}
  • Guess構造体を定義

    • i32を保持するvalueフィールドがある(数値を格納する場所)
  • Guessの値のインスタンスを作成するnew関数を実装

    • i32型のvalueパラメータを持ち、Guessを返すように定義
  • selfを借用し、他のパラメータを持たず、i32を返すvalueメソッドを実装

    • この種のメソッドはgetterと呼ばれる
    • 目的はフィールドからデータを取得して返すこと