Open3

Rust勉強手記ーエラーハンドリング

convers39convers39

rustのもう一つ特別なところ。エラーハンドリングの設計は他の言語と結構違う面がある。

一般的に使われているtry/catch構造でexceptionをキャッチするロジックは、rustには見えない。

rustでは、回復可能な(recoverable)と回復不可能(unrecoverable)なエラーを区別している。回復不可能な場合は、panic!マクロが実行され、いわばパニックが発生し、プログラムの実行が中止となる。逆に、回復可能な場合は、Result<T, E>をリターンする形で、エラーをResultEnum型に箱入りしている。

enum Result<T, E> {
    Ok(T),
    Err(E),
}

すると、この実行結果によって処理を分けることができるようになる。ある意味で強制的なので、よりrobustなコードになるでしょう。

Enum型なので、matchと併用する形が思い浮かぶかもしれない。より洗練な書き方、unwrapexpect、クロージャー、?演算子などもある。

    let greeting_file_result = File::open("hello.txt");

    let greeting_file = match greeting_file_result {
        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);
            }
        },
    };

    // よりシンプルに
    let greeting_file = File::open("hello.txt").unwrap();
convers39convers39

?を使うときによくエラーのpropagationと関連する。実際の開発にもよくある場面だと思うが、一々全ての関数にtry/catchを実装するよりも、ミドルウェアとかの形でどこかで統一キャッチしたりする。

rustで実践するときに注意したいのは、caller関数のリターンタイプと、?演算のリターンタイプの一致性だと。例えばmain関数のなかで全てのエラーを「キャッチ」しようとすると、

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")?;
}

this function should return Result or Option to accept ?とのエラーメッセージが出てしまう。このときに、File::open関数のリターン値はResult型なので、main関数も合わせる必要がある。

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

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

    Ok(())
}

この辺りまだ不慣れがあってもう少し時間がかかりそう。

convers39convers39

それでいつpanic!を使うか、いつResult<T, E>を使うか、との問題が出てくる。

これはtsで言えば、try/catchでcatchブロックでunknown型エラーに対してエラーの種類を一個ずつ確認して、それぞれの処理を行う場面を考えるとわかりやすいかもしれない。一般的に、この失敗・エラーは想定中の可能性の一つResultで考えた方が良いらしい。

もしくはコンパイラーより多くの情報を持つ場合、例えばreactのDOM操作で!をつけて要素が必ず存在するよという時はコンパイラーより開発者が多くの情報を持っている→大丈夫だとと確信を持っている時、Resultも使った方が良いみたい。

ただし、コードがbad state にある時は、パニックを採用する。この悪い状態というのは、公式でwhen some assumption, guarantee, contract, or invariant has been brokenと説明している。こちらのポストにもあるように、a fundamental assumption of the current code block is found to be falseの状態。言い換えれば、コードが予定通り、正しく実行できない状態とも解釈できる。

この辺りは以前設計原則のところと少し関係あるが、例えば契約的設計の前置き条件・後置き条件が破れたときは該当するでしょう。これらの条件は「コードが正しく実行する」ために設置されていると想定できるので、bad stateの基準にもなりそうかなと。

また、意図的にパニックしたいケースもあったりする。例えばテストの時、prototyping(ここは若干意味理解していないが、設計パターンのことかな)、ただただサンプルコードを書いている時など。