rust
failure

Rustのエラー処理

この記事はWanoアドベントカレンダーの5日目の記事です。

TL;DR

  • Result型の値の項の後ろに?を付けると例外っぽく振舞う
  • failureを使う

エラー処理の方法

Rustには例外がありません。
Go言語と同じように戻り値でエラーを返します(同様にpanicもあります)。

Rustでは『処理結果もしくはエラー内容』を表す型としてResult型というのがあります。
Result型はenumとして次の様に定義されています。

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

Ok(T)が正常に処理できた結果、Err(E)が何らかのエラーが発生したことを表します。

例えば、バイト列からStringを作るString::from_utf8メソッドは戻り値の型がResult<String, FromUtf8Error>です。
これはStringを作る処理が成功したらOk(String)が返り、不正なutf8バイト列を与える等するとErr(FromUtf8Error)が返る事を表しています。

呼び出し側はこのResultmatchによるパターンマッチか、Result型のメソッドのmap等を繋げるメソッドチェーンで処理します。

パターンマッチで処理する場合:

use std::fs;

fn main() {
    match fs::read_to_string("/file_does_not_exist") {
        Ok(s) => println!("Content => {}", s),
        Err(err) => eprintln!("IO Error => {}", err),
    }
}

メソッドチェーンで処理する場合:

use std::fs;

fn main() {
    fs::read_to_string("/file_does_not_exist")
        .map(|s| println!("Content => {}", s))
        .unwrap_or_else(|err| eprintln!("IO Error => {}", err));
}

どちらを使うかは書いている処理に合わせて選択します。

?オペレーター

エラー処理をする時に全てのエラーをその場で処理する事はそんなになく、呼び出し元にエラー処理を委ねる事が多いです。

これを普通に書くと下記の様にいちいちmatchしてreturnしなくてはならず、とても面倒です。

use std::fs;
use std::io;

fn load_2files_and_print() -> Result<(), io::Error> {
    match fs::read_to_string("/file_does_not_exist_1") {
        Ok(s) => println!("Content1 => {}", s),
        Err(err) => return Err(err),
    }

    match fs::read_to_string("/file_does_not_exist_2") {
        Ok(s) => println!("Content2 => {}", s),
        Err(err) => return Err(err),
    }

    Ok(())
}

fn main() {
    load_2files_and_print()
        .unwrap_or_else(|err| eprintln!("IO Error => {}", err));
}

そこでこの手間を減らす為に?オペレーターという機能があります。
これはResultを返す式の最後に?を書くと、Ok(T)ならばTの値を返し、Err(E)ならばErr(E)をreturnしてくれるという機能です。
また単にErr(E)をreturnするだけではなく、関数の戻り値型がResult<T, E2>の時にFrom::fromによる型変換も自動的に行ってくれます。

これを使うと上記のコードは次の様になります。

use std::fs;
use std::io;

fn load_2files_and_print() -> Result<(), io::Error> {
    println!("Content1 => {}", fs::read_to_string("/file_does_not_exist_1")?);
    println!("Content2 => {}", fs::read_to_string("/file_does_not_exist_2")?);
    Ok(())
}

fn main() {
    load_2files_and_print()
        .unwrap_or_else(|err| eprintln!("IO Error => {}", err));
}

圧倒的にシンプルになりました。

独自のエラー型を返す

ライブラリ等を書いていると独自のエラー型を返したくなる事があります。
そのような時はfailure crateを使ってエラー型を定義すると簡単に独自エラー型を用意できます。

failureはFail traitを提供しており、独自エラー型でこれを実装します。
Failは自動実装の為のマクロが提供されているので、#[derive(Fail)]とするだけでほとんどの実装を行ってくれます。
ほとんどの実装というのはDisplay traitの実装が実は必須なのですが、これを自動では行ってくれないのです。
しかし簡単にDisplay traitの実装を用意できる機能が提供されています。
下記のコード中の#[fail(display = "...", ...)]の様に記述する事で後はfailure側で自動的にDisplay traitを実装を行ってくれます。

#[macro_use]
extern crate failure;

use std::fs;
use std::io;

#[derive(Debug, Fail)]
enum MyError {
    // これでDisplayの実装を用意してくれる。
    // この構文の記法はprintln!マクロ等とほぼ同じで、
    // フォーマット文字列の後にはフィールド名を書きます。
    #[fail(display = "An IO error occured: {}", 0)]
    IoError(io::Error),
}

// Fromは自分で実装...
impl From<io::Error> for MyError {
    fn from(err: io::Error) -> MyError {
        MyError::IoError(err)
    }
}

// MyError型をエラー型として使えるようになった!
fn load_and_print() -> Result<(), MyError> {
    println!("{}", fs::read_to_string("/file_does_not_exist")?);
    Ok(())
}

fn main() {
    load_and_print().unwrap_or_else(|err| eprintln!("{}", err));
}

これをビルドして実行すると次の様な出力になります。

An IO error occured: No such file or directory (os error 2)

独自のエラー型を?オペレーターで返してエラーメッセージの表示まで出来ました。

failureのより詳しい使い方等は下記のドキュメントを参照してください。
https://boats.gitlab.io/failure/intro.html