Help us understand the problem. What is going on with this article?

Rustのエラー処理

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

2020/07現在の状況について

エラー処理周りは状況が少し変わりました。
具体的には標準ライブラリ(std)のstd::error::Errorにbacktraceに関する機能追加が行われ、failure::Failが不要になりました。
引き続きfailureを使っても良いのですが、最近はfailure::Error相当の物を使う為にanyhowを、std::error::Errorを簡単にimplする為にthiserrorを使うのがおすすめです。

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

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

独自のエラー型を返す

※ 2020/07/24追記: 現在ではthiserrorを使ってstd::error::Errorを実装するのが簡単でおすすめです。

ライブラリ等を書いていると独自のエラー型を返したくなる事があります。
そのような時は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

fujitayy
wano-inc
「Cultivate your dream」をミッションにクリエイター・アーティストを支援する事業やサービスを展開
https://wano.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした