Edited at

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