この記事は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)
が返る事を表しています。
呼び出し側はこのResult
をmatch
によるパターンマッチか、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