Open8

[rust] エラーハンドリングに関する知識まとめ

shiratorishiratori

Rustのエラー処理周りがいろいろ用語が出てきたりして混乱するので、体系的に捉えたくてマインドマップにまとめてみた。

shiratorishiratori

エラー処理 - The Rust Programming Language 日本語版

Rustでは、エラーは大きく二つに分類される。回復可能回復不能なエラー。

Rustでは、エラーは大きく二つに分類されます: 回復可能回復不能なエラーです。 ファイルが見つからないなどの回復可能なエラーには、問題をユーザに報告し、処理を再試行することが合理的になります。 回復不能なエラーは、常にバグの兆候です。例えば、配列の境界を超えた箇所にアクセスしようとすることなどです。
多くの言語では、この2種のエラーを区別することはなく、例外などの機構を使用して同様に扱います。 Rustには例外が存在しません。代わりに、回復可能なエラーにはResult<T, E>値があり、 プログラムが回復不能なエラーに遭遇した時には、実行を中止するpanic!マクロがあります。

shiratorishiratori

panic!で回復不能なエラー - The Rust Programming Language 日本語版

panic!で回復不能なエラー

バックトレースか、アボート

Rustにはpanic!マクロが用意されています。panic!マクロが実行されると、 プログラムは失敗のメッセージを表示し、スタックを巻き戻し掃除して、終了します。これが最もありふれて起こるのは、 何らかのバグが検出された時であり、プログラマには、どうエラーを処理すればいいか明確ではありません。

パニックに対してスタックを巻き戻すか異常終了するか
標準では、パニックが発生すると、プログラムは巻き戻しを始めます。つまり、言語がスタックを遡り、 遭遇した各関数のデータを片付けるということです。しかし、この遡りと片付けはすべきことが多くなります。 対立案は、即座に異常終了し、片付けをせずにプログラムを終了させることです。そうなると、プログラムが使用していたメモリは、 OSが片付ける必要があります。プロジェクトにおいて、実行可能ファイルを極力小さくする必要があれば、 Cargo.tomlファイルの適切な[profile]欄にpanic = 'abort'を追記することで、 パニック時に巻き戻しから異常終了するように切り替えることができます。

バックトレースとは、ここに至るまでに呼び出された全関数の一覧です。

shiratorishiratori

Resultで回復可能なエラー - The Rust Programming Language 日本語版

Resultで回復可能なエラー

matchを使ってパターンマッチ

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

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

    let f = match f {
        Ok(file) => file,
        Err(ref error) if error.kind() == ErrorKind::NotFound => {
            match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => {
                    panic!(
                        //ファイルを作成しようとしましたが、問題がありました
                        "Tried to create file but there was a problem: {:?}",
                        e
                    )
                },
            }
        },
        Err(error) => {
            panic!(
                "There was a problem opening the file: {:?}",
                error
            )
        },
    };
}

if error.kind() == ErrorKind::Notfoundという条件式は、マッチガードと呼ばれます: アームのパターンをさらに洗練するmatchアーム上のおまけの条件式です。

エラー時にパニックするショートカット: unwrapとexpect

matchの使用は、十分に仕事をしてくれますが、いささか冗長になり得る上、必ずしも意図をよく伝えるとは限りません。 Result<T, E>型には、色々な作業をするヘルパーメソッドが多く定義されています。それらの関数の一つは、 unwrapと呼ばれますが、リスト9-4で書いたmatch式と同じように実装された短絡メソッドです。 Result値がOk列挙子なら、unwrapはOkの中身を返します。ResultがErr列挙子なら、 unwrapはpanic!マクロを呼んでくれます。こちらが実際に動作しているunwrapの例です:

use std::fs::File;

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

expectは、unwrapに似ていますが、panic!のエラーメッセージも選択させてくれます。 unwrapの代わりにexpectを使用して、いいエラーメッセージを提供すると、意図を伝え、 パニックの原因をたどりやすくしてくれます。expectの表記はこんな感じです:

use std::fs::File;

fn main() {
    // hello.txtを開くのに失敗しました
    let f = File::open("hello.txt").expect("Failed to open hello.txt");
}

エラーを委譲する

失敗する可能性のある何かを呼び出す実装をした関数を書く際、関数内でエラーを処理する代わりに、 呼び出し元がどうするかを決められるようにエラーを返すことができます。これはエラーの委譲として認知され、 自分のコードの文脈で利用可能なものよりも、 エラーの処理法を規定する情報やロジックがより多くある呼び出し元のコードに制御を明け渡します。

例えば、リスト9-6の関数は、ファイルからユーザ名を読み取ります。ファイルが存在しなかったり、読み込みできなければ、 この関数はそのようなエラーを呼び出し元のコードに返します。


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

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),
    }
}

(リスト9-6: matchでエラーを呼び出し元のコードに返す関数)

Rustにおいて、この種のエラー委譲は非常に一般的なので、Rustにはこれをしやすくする?演算子が用意されています。

エラー委譲のショートカット: ?演算子


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

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)
}

Result値の直後に置かれた?は、リスト9-6でResult値を処理するために定義したmatch式とほぼ同じように動作します。 Resultの値がOkなら、Okの中身がこの式から返ってきて、プログラムは継続します。値がErrなら、 returnキーワードを使ったかのように関数全体からErrの中身が返ってくるので、 エラー値は呼び出し元のコードに委譲されます。

リスト9-6のmatch式と?演算子には違いがあります: ?を使ったエラー値は、 標準ライブラリのFromトレイトで定義され、エラーの型を別のものに変換するfrom関数を通ることです。 ?演算子がfrom関数を呼び出すと、受け取ったエラー型が現在の関数の戻り値型で定義されているエラー型に変換されます。これは、 個々がいろんな理由で失敗する可能性があるのにも関わらず、関数が失敗する可能性を全て一つのエラー型で表現して返す時に有用です。 各エラー型がfrom関数を実装して返り値のエラー型への変換を定義している限り、 ?演算子が変換の面倒を自動的に見てくれます。

リスト9-7の文脈では、File::open呼び出し末尾の?はOkの中身を変数fに返します。 エラーが発生したら、?演算子により関数全体から早期リターンし、あらゆるErr値を呼び出し元に与えます。 同じ法則がread_to_string呼び出し末尾の?にも適用されます。

?演算子により定型コードの多くが排除され、この関数の実装を単純にしてくれます。 リスト9-8で示したように、?の直後のメソッド呼び出しを連結することでさらにこのコードを短くすることさえもできます。

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

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)
}

?演算子は、Resultを返す関数でしか使用できない

?演算子は戻り値にResultを持つ関数でしか使用できません。というのも、リスト9-6で定義したmatch式と同様に動作するよう、 定義されているからです。Resultの戻り値型を要求するmatchの部品は、return Err(e)なので、 関数の戻り値はこのreturnと互換性を保つためにResultでなければならないのです。

shiratorishiratori

Option型

std::option - Rust

Type Option はオプションの値を表します。すべての Option は、Some で値を含むか、None で値を含まないかのいずれかです。 オプションの型は、多くの用途があるため、Rust コードでは非常に一般的です。

  • 初期値
  • 入力範囲全体で定義されていない関数 (部分関数) の戻り値
  • それ以外の場合は単純なエラーを報告するための戻り値。エラーの場合は None が返されます
  • オプションの構造体フィールド
  • 借用または「取得」できる構造体フィールド
  • オプションの関数引数
  • Null 許容ポインター
  • 困難な状況からの脱却

Rustのエラーハンドリングガイド!Option型やResult型を使いこなす - テックブログです

Option<T>型は取得できないかもしれない値を表現する列挙型。
Noneはエラーではない。値がなかったことを示すだけ。

pub enum Option<T> {
    None,
    Some(T),
}

Some

Some(T)は何かしらの型を1つ持った、匿名タプル構造体型である。
Option in std::option - Rust

let some1 = Some(5);
let some2 = Some("hello");

Someの引数はi32型, &str型など、何でもいい。

None

Noneは他言語でいうところのNULL相当。enumOption<T>の値のひとつであり、引数に構造体をとらない。

let nullable_int: Option<i32> = None;
nullable_int = Some(100);

変数nullable_intはi32型だが、NULLも許容したい。そんなときにOption<i32>型を使う。
以下はエラーになる。

nullable_int = 100; // error
nullable_int = Some("hello"); // error
let sum = Some(5) + 1; // error