Charaken 技術系ブログ

技術系に関して学んだことや、気になったことを記事にしていく。

【Rust】がばがばRust独学 - 9. Error Handling

f:id:charaken:20191223210559p:plain

Rustの公式ドキュメントを通して、Rustを独学していく記事になります。(がば要素あり)

doc.rust-lang.org

今回は、Error Handlingについて学びつつ、気になった点をコード確認した結果を記事にしています。

  • versions
$ rustc --version
rustc 1.40.0 (73528e339 2019-12-16)


Error Handling

公式ドキュメント(Error Handling - The Rust Programming Language)より、エラーには二種類存在します。

  • recoverable : ファイルが見つからないなどのエラー
  • unrecoverable :バグなどのエラー

ただ、ほとんどの言語では2種類のエラーを区別せずに、例外を利用して同じ方法で処理します。

Rustには例外は無く、

  • recoverableResult<T, E> による回復可能なエラーのタイプ
  • unrecoverablepanic!による実行を停止するマクロ

の二種類が存在します。

Rust groups errors into two major categories: recoverable and unrecoverable errors.
...
Most languages don’t distinguish between these two kinds of errors and handle both in the same way, using mechanisms such as exceptions. Rust doesn’t have exceptions.

Unrecoverable Errors with panic!

Unrecoverable Errors with panic! - The Rust Programming Language

Rustのデフォルトでは、パニックが発生するとプログラムの巻き戻し(unwinding)が開始されて、各関数のデータをクリーンアップします。クリーンアップさせない場合はabortを下記のように設定することにより、巻き戻しが発生しません。巻き戻しを発生させない分、プロジェクトのバイナリを小さくするとが可能ですが、メモリのクリーンアップは自身で実施する必要があります。

[profile.release]
panic = 'abort'

※ クリーンアップに関しては別記事でまとめたいと考えております。

panic! の単純な使用例
fn main() {
    panic!("crash and burn");
}
thread 'main' panicked at 'crash and burn', src/demo.rs:2:5
panic! のバックトレースを使用

例えば、下記の場合、実行時エラーより、 RUST_BACKTRACE=1環境変数として実行することにより、バックトレースを可能と記載があります。

fn main() {
    let v = vec![1, 2, 3];
    v[99];
}
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', /rustc/73528e339aae0f17a15ffa49a8ac608f50c6cf14/src/libcore/slice/mod.rs:2796:10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

実際に、 RUST_BACKTRACE=1環境変数として実行すると、下記のようにバックトレースしてくれます。

bash-3.2$ RUST_BACKTRACE=1 cargo run --bin demo
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/demo`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', /rustc/73528e339aae0f17a15ffa49a8ac608f50c6cf14/src/libcore/slice/mod.rs:2796: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:77
   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:1412
   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:188
   9: std::panicking::default_hook
             at src/libstd/panicking.rs:205
  10: std::panicking::rust_panic_with_hook
             at src/libstd/panicking.rs:464
  11: std::panicking::continue_panic_fmt
             at src/libstd/panicking.rs:373
  12: rust_begin_unwind
             at src/libstd/panicking.rs:302
  13: std::panicking::begin_panic
  14: std::panicking::begin_panic
  15: <usize as core::slice::SliceIndex<[T]>>::index
             at /rustc/73528e339aae0f17a15ffa49a8ac608f50c6cf14/src/libcore/slice/mod.rs:2796
  16: core::slice::<impl core::ops::index::Index<I> for [T]>::index
             at /rustc/73528e339aae0f17a15ffa49a8ac608f50c6cf14/src/libcore/slice/mod.rs:2647
  17: <alloc::vec::Vec<T> as core::ops::index::Index<I>>::index
             at /rustc/73528e339aae0f17a15ffa49a8ac608f50c6cf14/src/liballoc/vec.rs:1861
  18: demo::main
             at src/demo.rs:3
  19: std::rt::lang_start::{{closure}}
             at /rustc/73528e339aae0f17a15ffa49a8ac608f50c6cf14/src/libstd/rt.rs:61
  20: std::rt::lang_start_internal::{{closure}}
             at src/libstd/rt.rs:48
  21: std::panicking::try::do_call
             at src/libstd/panicking.rs:287
  22: __rust_maybe_catch_panic
             at src/libpanic_unwind/lib.rs:78
  23: std::panicking::try
             at src/libstd/panicking.rs:265
  24: std::panic::catch_unwind
             at src/libstd/panic.rs:396
  25: std::rt::lang_start_internal
             at src/libstd/rt.rs:47
  26: std::rt::lang_start
             at /rustc/73528e339aae0f17a15ffa49a8ac608f50c6cf14/src/libstd/rt.rs:61
  27: demo::main
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

これにより、libcore/slice/mod.rs:2796:10 としてpanicに対して demo::main の3行目でのエラーと判明させることができます。

  18: demo::main
             at src/demo.rs:3

Recoverable Errors with Result

Recoverable Errors with Result - The Rust Programming Language

Result は下記のように設定されています。

enum Result<T, E> {
    Ok(T),
    Err(E),
}
Result の基本的な使い方

例えば、公式ドキュメントのデモコードから少し改変したコードを利用し、わざと hello.txt がない場合を出力してみます。そうすると、 Err として返ってきており、 Os レベルでのエラーとわかります。

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");
    println!("{:#?}", f);
}
Err(
    Os {
        code: 2,
        kind: NotFound,
        message: "No such file or directory",
    },
)

ファイルが存在するパターンを利用した場合は、下記のように Ok で返ってきます。

use std::fs::File;

fn main() {
    let f = File::open("Cargo.toml");
    println!("{:#?}", f);
}
Ok(
    File {
        fd: 3,
        path: "/path/to/Cargo.toml",
        read: true,
        write: false,
    },
)
matchを利用した Result の制御

Ok Err はmatchによって制御可能です。

公式ドキュメントの例より、 hello.txt が存在しない場合は、ファイルを作成するというものになります。

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = File::open("hello.txt");
    let file = 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),
        },
    };

    println!("{:#?}", file);
}
File {
    fd: 3,
    path: "/path/to/hello.txt",
    read: false,
    write: true,
}

ioErrorKind については、下記のように様々なエラーが用意されております。

doc.rust-lang.org

unwrapexpect のPanicショートカット
  • unwrap : メッセージ変更不可
  • expect : メッセージ変更可

unwrap を利用した場合、 Err の場合は Err(E)E を出力してくれます。

use std::fs::File;

fn main() {
    let f = File::open("hello.txt").unwrap();
    println!("{:#?}", f);
}
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/libcore/result.rs:1165:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

Ok の場合の unwrapOk(T)T を返してくれます。

use std::fs::File;

fn main() {
    let f = File::open("Cargo.toml").unwrap();
    println!("{:#?}", f);
}
File {
    fd: 3,
    path: "/path/to/Cargo.toml",
    read: true,
    write: false,
}

expectunwrap と同様に ErrOk を返してくれます。

異常系の場合

use std::fs::File;

fn main() {
    let f = File::open("hello.txt").expect("Failed to open hello.txt");
    println!("{:#?}", f);
}
thread 'main' panicked at 'Failed to open hello.txt: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/libcore/result.rs:1165:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

正常系の場合

use std::fs::File;

fn main() {
    let f = File::open("Cargo.toml").expect("Failed to open Cargo.toml");
    println!("{:#?}", f);
}
File {
    fd: 3,
    path: "/path/to/Cargo.toml",
    read: true,
    write: false,
}
エラーの伝播

関数の戻り値として Result を呼び出し元に伝播することが可能です。

fn read_username_from_file() -> Result<String, io::Error>

? を利用することにより、実行段階での Err を判別し、すぐさま戻してくれます。

use std::fs::File;
use std::io;
use std::io::Read;

fn main() {
    let f = read_username_from_file("hello.txt");
    println!("{:#?}", f);
}

fn read_username_from_file(file_name: &str) -> Result<String, io::Error> {
    let mut s = String::new();
    File::open(file_name)?.read_to_string(&mut s)?;
    Ok(s)
}
main 関数でのResultの返し方

main 関数の戻り値はデフォルトで () であるため、 Result を扱いたい場合には下記のように設定することで返すことができます。 Box<dyn Error> タイプについては、 公式ドキュメント Chapter 17 に記載されているそうです。

use std::error::Error;
use std::fs::File;

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

    Ok(())
}
Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }

To panic! or Not to panic!

To panic! or Not To panic! - The Rust Programming Language

公式ドキュメントより引用すると、仮定・保証・不変式が壊れっる場合はパニックになることをおすすめするとのことです。

It’s advisable to have your code panic when it’s possible that your code could end up in a bad state. In this context, a bad state is when some assumption, guarantee, contract, or invariant has been broken, such as when invalid values, contradictory values, or missing values are passed to your code—plus one or more of the following:

・The bad state is not something that’s expected to happen occasionally.
・Your code after this point needs to rely on not being in this bad state.
・There’s not a good way to encode this information in the types you use.

失敗が予測できる場合は、Result を利用すべきだとも記載されています。

However, when failure is expected, it’s more appropriate to return a Result than to make a panic! call.

また、値チェックを始めに実施し、おかしければpanicを起こすべきとも記載されています。

When your code performs operations on values, your code should verify the values are valid first and panic if the values aren’t valid.


最後に

長くなってしまいましたが、Error Handlingについて学びました。適切に panic! を使って動作を止める方が懸命である場合も有るとのことですね・・・

個人的には、web APIとして利用する場合は、 panic! を引き起こすとアプリケーションが止まってしまい、サービスへの影響が大きいため、 Result で伝搬して行くほうが懸命だと考えております。

webサービスpanic! を利用する場合はどんな場合なのでしょうか・・・思いつきません・・・

次回はgenericsですね。型の上限境界、下限境界はあるのかが気になる点です。