この記事の概要
Option<T>
型や Result<T, E>
型の戻り値を、match
式で判別している以下のようなコードを、
fn double_arg(mut argv: env::Args) -> Result<i32, String> {
match argv.nth(1) {
None => Err("数字を1つ指定してください。".to_owned()),
Some(arg1) => {
match arg1.parse::<i32>() {
Ok(n) => Ok(2 * n),
Err(err) => Err(err.to_string()),
}
}
}
}
Rust の標準的な部品を組み合わせて、以下のように、すっきり読みやすく仕上げるまでの道のりを紹介します。
fn double_arg(mut argv: env::Args) -> Result<i32, CliError> {
let arg1 = try!(argv.nth(1).ok_or(CliError::NotEnoughArgs));
let n = try!(arg1.parse::<i32>());
Ok(2 * n)
}
なお、この記事は、Rust の公式ドキュメント「Rustプログラミング言語」(日本語翻訳版)の エラーハンドリング の章を参考にして書きました。
Rust のバージョン
この記事のコードは、2016年2月現在の最新の安定版で動作確認済みです:
- rustc 1.6.0
- cargo 0.8.0 (08da2f5 2015-12-21)
rustc 1.0 以降なら、どのバージョンでも問題なく動くと思います。
ソースコード
本記事で紹介するソースコードは、GitHub に置いてあります。
Rust と Cargo がインストールされた環境で、以下のようにすれば実行できます。
$ cd rust-option-result-examples
$ cargo --bin main{1-7の連番} -- プログラムへの引数
例:
$ cargo --bin main7 -- 123
Compiling error-handling v0.1.0 (file:/// ... rust-option-result-examples)
Running `target/debug/main7 123`
246
基礎知識:Option<T>
型と Result<T, E>
型
Option<T>
型
Rust の Option<T>
型は、値が 存在しない 可能性を暗示する列挙型です。定義は以下の通りです。
enum Option<T> {
None,
Some(T),
}
T型の値が存在する時は、Some
で包まれています。存在しない時は None
になります。
Option
型は、他の言語では Maybe
や Optional
などと呼ばれることもあります。
例:
Linux の Docker が使える環境で rusti を実行:
% sudo docker run -it --rm quay.io/tatsuya6502/rusti
# cd ~/rusti
# cargo run
rusti=>
Some(10)
は Option<i32>
型です。
rusti=> Some(10)
Some(10)
rusti=> .type Some(10)
Some(10) = core::option::Option<i32>
注意: rusti で型を見ると core
ライブラリの Option<T>
型が優先されてしまいます。通常の Rust プログラムでは core
ライブラリを use
指定してないので、core
ではなく、std
ライブラリの std::option::Option<T>
型になります。
こちらは Option<String>
型です。
rusti=> Some("Hello, world!".to_owned())
Some("Hello, world!")
rusti=> .type Some("Hello, world!".to_owned())
...省略... = core::option::Option<collections::string::String>
None
は単体だと Option<T>
の T
が決まらないので、コンパイルエラーになります。
rusti=> None
<anon>:13:20: 13:24 error: unable to infer enough type information about `_`; type annotations or generic parameter binding required [E0282]
...
ヒントを与えましょう。
rusti=> None as Option<i32>
None
rusti=> .type None as Option<i32>
... = core::option::Option<i32>
Some
に包まれた値を取り出すには、match
式による場合分けをします。
rusti=> .block
rusti+> let v = Some(10);
rusti+> match v {
rusti+> Some(n) => n,
rusti+> None => -1,
rusti+> }
rusti+> .
10
Option
に出会う度に場合分けをするのは面倒なので、標準ライブラリでは、Option
から値を取り出したり、加工したりするためのメソッドが用意されています。例えば unwrap()
は最も素朴なメソッドで、以下のような定義になっています:
impl<T> Option<T> {
fn unwrap(self) -> T {
match self {
Option::Some(val) => val,
Option::None =>
panic!("called `Option::unwrap()` on a `None` value"),
}
}
}
このように Some<T>
に適用すると、値が取り出せます。
rusti=> Some(10).unwrap()
10
そして、None
に適用すると、パニックします。
rusti=> (None as Option<i32>).unwrap()
thread '<main>' panicked at 'called `Option::unwrap()` on a `None` value', ../src/libcore/option.rs:330
map()
はクロージャを引数にとり、Some
の中の値にクロージャを適用して、結果を Some
で包み直します。もし None
に適用すると、None
を返します。
rusti=> Some(10).map(|n| 2 * n)
Some(20)
rusti=> (None as Option<i32>).map(|n| 2 * n)
None
and_then()
は map()
に似ていますが、違いは Some<T>
だけでなく None
も返せることです。
rusti=> Some(10).and_then(|n| if n > 5 {Some(n)} else {None} )
Some(10)
rusti=> Some(5).and_then(|n| if n > 5 {Some(n)} else {None} )
None
rusti=> (None as Option<i32>).and_then(|n| if n > 5 {Some(n)} else {None} )
None
map()
や and_then()
は入力と出力に Option<T>
型の値を1つだけ取るので、数珠つなぎにできます。このような性質を持つメソッドを、コンビネータと呼びます。
rusti=> Some(10).and_then(|n| if n > 5 {Some(n)} else {None}).map(|n| 2 * n)
Some(20)
rusti=> Some(5).and_then(|n| if n > 5 {Some(n)} else {None}).map(|n| 2 * n)
None
Result<T, E>
型
Rust の Result<T, E>
型は エラーになる可能性 を暗示する列挙型です。定義は以下の通りです。
enum Result<T, E> {
Ok(T),
Err(E),
}
処理が成功した時は値を Ok
で包みます。失敗した時はエラーの値を Err
で包みます。
Result
型は、他の言語では Either
と呼ばれることもあります。
rusti=> .type Ok(10) as Result<i32, ()>
... = core::result::Result<i32, ()>
rusti=> .type Ok("Success!".to_owned()) as Result<String, ()>
... = core::result::Result<collections::string::String, ()>
rusti=> .type Err("Not found".to_owned()) as Result<i32, String>
... = core::result::Result<i32, collections::string::String>
Option<T>
と同じようなメソッドが用意されています。unwrap()
は値が Err
の時はパニックします。
rusti=> (Ok(10) as Result<i32, String>).unwrap()
10
rusti=> (Err("Not found".to_owned()) as Result<i32, String>).unwrap()
thread '<main>' panicked at 'called `Result::unwrap()` on an `Err` value: "Not found"', ../src/libcore/result.rs:746
and_then()
や map()
は、値が Ok<T>
のときはクロージャを実行し、値が Err<E>
のときほ元の値を未加工で返します。
rusti=> .block
rusti+> (Ok(10) as Result<i32, String>)
rusti+> .and_then(|n| if n > 5 {Ok(n)} else {Err("Too small".to_owned())})
rusti+> .map(|n| 2 * n)
rusti+> .
Ok(20)
rusti=> .block
rusti+> (Ok(5) as Result<i32, String>)
rusti+> .and_then(|n| if n > 5 {Ok(n)} else {Err("Too small".to_owned())})
rusti+> .map(|n| 2 * n)
rusti+> .
Err("Too small")
また、逆に値が Err
のときだけ働くメソッドもあります。
rusti=> .block
rusti+> (Err("Not found".to_owned()) as Result<i32, String>)
rusti+> .map_err(|e| "Error! ".to_owned() + &e)
rusti+> .
Err("Error! Not found")
明示的な場合分けから、try!
マクロとコンビネータへ
では、冒頭にあげたプログラムを例に、Option<T>
や Result<T, E>
の扱いを、少しずつ改善していきましょう。このプログラムは、コマンドライン引数として整数値を表す文字列を受け取り、それを2倍した値を表示します。
引数として "10"
を与えた時:
% cargo run --bin main1 -- 10
Running `target/debug/main1 10`
20
unwrap()
を使う
まず最初は、エラーの可能性は考慮せず、正しい引数が渡されたときの機能だけを実装します。以下のように double_arg()
関数は、コマンドライン引数の1番目の値を取得して、整数値としてパースし、2倍して返します。
use std::env;
fn double_arg(mut argv: env::Args) -> i32 {
let arg1 = argv.nth(1).unwrap(); // エラー1
let n = arg1.parse::<i32>().unwrap(); // エラー2
2 * n
}
そして呼び出し元となる main()
関数では、double_arg()
を呼び出して、結果を表示するようにしました。
fn main() {
let n = double_arg(env::args());
println!("{}", n);
}
このプログラムは、正しい引数を与えれば問題なく動きます。
% cargo run --bin main1 -- 42
Running `target/debug/main1 42`
84
しかし当然、エラーになるとパニックを起こしてクラッシュします。
まず、引数を指定しないと、先ほどのソースコードのエラー1の行でパニックします。
% cargo run --bin main1
Running `target/debug/main1`
thread '<main>' panicked at 'called `Option::unwrap()` on a `None` value', src/libcore/option.rs:367
Process didn't exit successfully: `target/debug/main1` (exit code: 101)
メッセージの意味は、None
に対して、Option::unwrap()
が呼ばれました、です。argv.nth(1)
は Option<String>
型の値を返します。コマンドライン引数のインデックス1の文字列があれば Some
が返され、なければ None
を返します。
rusti=> use std::env;
rusti=> .type env::args().nth(1)
... = core::option::Option<collections::string::String>
また、引数が整数としてパースできないときも、エラー2の行でパニックします。
% cargo run --bin main1 -- hoge
Running `target/debug/main1 hoge`
thread '<main>' panicked at 'called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }', src/libcore/result.rs:741
Process didn't exit successfully: `target/debug/main1 hoge` (exit code: 101)
メッセージの意味は、Err
に対して、Result::unwrap()
が呼ばれました、です。parse()
は、(この場合は i32
型の結果が期待されていますので) Result<i32, std::num::ParseIntError>
型の値を返します。パースできれば Ok<i32>
を、できなければ Err<std::num::ParseIntError>
を返します。
rusti=> .type "10".parse::<i32>()
... = core::result::Result<i32, core::num::ParseIntError>
明示的な場合分けをする
では、この2つのエラーに対応できるようにしましょう。double_arg()
関数は unwrap()
でパニックする代わりに、エラーが起こったことを、戻り値を使って呼び出し元(main()
関数)に伝えるようにします。リターン型が i32
のままだとエラーを表現できないので、Result<i32, String>
に変更します。エラーの内容は String
型の文字列で渡します。
まずは素朴なやり方として、match
式によるパターンマッチ、つまり、明示的な場合分けをしましょう。
use std::env;
fn double_arg(mut argv: env::Args) -> Result<i32, String> {
match argv.nth(1) {
None => Err("数字を1つ指定してください。".to_owned()),
Some(arg1) => {
match arg1.parse::<i32>() {
Ok(n) => Ok(2 * n),
Err(err) => Err(err.to_string()),
}
}
}
}
このように、もし argv.nth(1)
が None
を返したら、「数字を1つ指定してください」という String
型の文字列を Err()
で包んで返します。
またもし parse::<i32>()
が Err<std::num::ParseIntError>
を返したら、ParseIntError
を to_string()
で String
型の文字列に変換してから、Err<String>
で包んで返します。
エラーがなければ、数値を2倍した値を Ok<i32>
型で返します。
main()
関数は double_arg()
関数からの戻り値に対して match
式で場合分けをして、必要ならエラーメッセージを表示します。
fn main() {
match double_arg(env::args()) {
Ok(n) => println!("{}", n),
Err(err) => println!("エラー:{}", err),
}
}
試してみましょう。
引数を忘れた時:
% cargo run --bin main2
Running `target/debug/main2`
エラー:数字を1つ指定してください。
引数が整数としてパースできない時:
% cargo run --bin main2 -- hoge
Running `target/debug/main2 hoge`
エラー:invalid digit found in string
どちらのケースもパニックしなくなりました。後者のメッセージは英語ですが、期待通りに動いてます。
この方法の長所は、プログラムの内容が見たままで、なにも抽象化されていないので、初心者にとってわかりやすいことです。短所は、エラーケースの場合分けが増えてくると、正常ケースのロジックがそれに埋もれてしまい、分かりにくくなってしまうことです。
最初はこのような書き方でもかまわないと思いますが、Rust に慣れてきたら、この後紹介するコンビネータや try!
マクロを使った方法へ移行していくことをお勧めします。
コンビネータを使う
明示的な場合分けを、コンビネータに肩代わりしてもらいましょう。このように書き換えられます。
use std::env;
fn double_arg(mut argv: env::Args) -> Result<i32, String> {
argv.nth(1)
.ok_or("数字を1つ指定してください。".to_owned())
.and_then(|arg1| arg1.parse::<i32>().map_err(|err| err.to_string()))
.map(|n| 2 * n)
}
fn main() {
// main2.rs と同じ。省略
}
Option<T>
の ok_or()
コンビネータは、argv.nth(1)
が返した Option<T>
型の値を、Result<T, E>
型に変換してくれます。もし値が Some<T>
なら Ok<T>
を作ります。もし値が None
なら Err<E>
を作って、引数として渡した値(この場合は String
型の "数字を1つ指定してください。"
)を包みます。
rusti=> Some("10").ok_or("error!".to_owned())
Ok("10")
rusti=> (None as Option<i32>).ok_or("error!".to_owned())
Err("error!")
Result<T, E>
の and_then()
、map_err()
、map()
コンビネータは先ほど紹介した通りです。
流れを追ってみましょう。
コマンドライン引数に "10"
が与えられたとき:
-
argv.nth(1)
がSome("10")
を返す。 -
ok_or()
コンビネータが、Ok("10")
に変換する。 -
and_then()
コンビネータが、Ok<String>
の中の値"10"
を取り出し、クロージャに渡す。 - クロージャは
"10"
にarg1.parse::<i32>()
を適用して、Ok(10)
が得られる。 - 同クロージャの
map_err()
コンビネータは何もせず、Ok(10)
を返す。 -
map()
コンビネータはOk(10)
の中の値10
を取り出して、クロージャに渡す。 - クロージャはそれを2倍して
20
を返す。 -
map()
コンビネータは、それを包み直してOk(20)
を返す。この値がdouble_arg()
の戻り値になる。
コマンドライン引数が与えられなかったとき:
-
argv.nth(1)
はNone
を返す。 -
ok_or()
コンビネータはErr<String>
型の値Err("数字を1つ...")
を返す。 - 後続の
and_then()
とmap()
は何もせず、Err<String>
値をそのまま返す。(double_arg()
の戻り値)
コマンドライン引数に、数値として無効な値 "hoge"
が与えられたとき:
-
and_then()
コンビネータのクロージャで"hoge".parse::<i32>()
が実行されErr<std::num::ParseIntError>
型の値が返る。 -
map_err()
コンビネータがエラーの値にto_string()
を適用して、Err<String>
型の値が作られる。 -
map()
コンビネータは何もせず、Err<String>
値をそのまま返す。(double_arg()
の戻り値)
どうでしょう? たしかに行数は少なくなりましたし、プログラムの文面は英語の文章っぽくなりました。しかし、クロージャが頻出するので個人的には、少し読みにくくなったように思えます。慣れの問題かもしれませんが。
次の try!
マクロとコンビネータを組み合わせる方法を見てみましょう。
try!
マクロとコンビネータを組み合わせる
Rust らしいやり方は、try!
マクロとコンビネータを適度に組み合わせることです。try!()
マクロは、コンビネータと同様に場合分けを肩代わりしてくれます。しかし、それだけでなく「早期リターン(early return)」という、制御フローの抽象化もしてくれます。
use std::env;
fn double_arg(mut argv: env::Args) -> Result<i32, String> {
let arg1 = try!(argv.nth(1).ok_or("数字を1つ指定してください。".to_owned()));
let n = try!(arg1.parse::<i32>().map_err(|err| err.to_string()));
Ok(2 * n)
}
fn main() {
// main2.rs と同じ。省略
}
try!()
マクロは、Result<T, E>
型の値を返す式を取ります。式を評価して Ok<T>
の値が得られた時は、unwrap()
のように、中の値を取り出して返します。一方、Err<E>
の値が得られた時は、return
文を使って double_arg()
関数から抜け出して、呼び出し元(main()
)にそのエラーの値を返します。
流れを追ってみましょう。
コマンドライン引数として "10"
が与えられたとき:
-
argv.nth(1)
がSome("10")
を返す。 -
ok_or()
コンビネータが、Ok("10")
に変換する。 -
try!()
マクロが中の値"10"
を取り出す。 - 変数
arg1
が"10"
に束縛される。 -
"10"
にparse::<i32>()
を適用するとOk(10)
が得られる。 -
map_err()
コンビネータは、Ok(10)
をそのまま返す。 -
try!()
マクロはOk(10)
の中の値10
を取り出す。 - 変数
n
が10
に束縛される。 -
2 * n
の結果をOk<i32>
に格納する。(double_arg()
の戻り値)
コマンドライン引数が与えられなかったとき:
-
argv.nth(1)
がNone
を返す。 -
ok_or()
コンビネータはErr<String>
型の値Err("数字を1つ...")
を返す。 -
try!()
マクロはreturn Err("数字を1つ...");
を実行し、main()
関数へ早期リターンする。
コマンドライン引数に、数値として無効な値 "hoge"
が与えられたとき:
- 変数
arg1
が"10"
に束縛されるところまでは、正常系と同じ。 -
"hoge".parse::<i32>()
がErr<std::num::ParseIntError>
型の値を返す。 -
map_err()
コンビネータがエラーの値にto_string()
を適用して、Err<String>
型の値が作られる。 -
try!()
マクロがreturn ...
を実行し、main()
関数へ早期リターンする。
早期リターンという手法が加わったことで、コードがすっきりしたと思いませんか? エラー処理をしてなかった頃のコードと比較してみましょう。
エラー処理なし
fn double_arg(mut argv: env::Args) -> i32 {
let arg1 = argv.nth(1).unwrap(); // エラー1
let n = arg1.parse::<i32>().unwrap(); // エラー2
2 * n
}
エラー処理あり
fn double_arg(mut argv: env::Args) -> Result<i32, String> {
let arg1 = try!(argv.nth(1).ok_or("数字を1つ指定してください。".to_owned()));
let n = try!(arg1.parse::<i32>().map_err(|err| err.to_string()));
Ok(2 * n)
}
エラー処理を加えた後でも、コードの明快さは失われてません。正常ケースのロジックがはっきりとわかりますし、その一方で、どこでエラーが起こりうるか、また、エラーが起こったらどうするのかもわかります。
エラー情報を改善する
いままで double_arg()
ではエラーが起こった時に Err<String>
型の値を返していました。これは手軽な方法ですし、今回のような短いプログラムでは十分、実用に耐えられます。でも残念なのは、parse::<i32>()
がせっかく Err<std::num::ParseIntError>
型の値を返しているのに、それを文字列に変換してから返していることです。
いったんエラーが文字列に変換されてしまうと、呼び出し元では場合分けがしにくくなってしまいます。例えば呼び出し元の main()
関数で、引数が指定されなかった時と、数値として無効な値が与えられた時で、別の動作をさせたくなったらどうしますか?また、エラーメッセージを日本語にローカライズしたい時もやりにくいでしょう。
もし double_arg()
が Err<std::num::ParseIntError>
のようにエラーの値をそのまま返すなら、この問題は解決します。
Box<Error>
型を返す
エラーの値を直接返したいと思った時に問題になるのは、double_arg()
では2種類のエラーが起こる可能性があることです。一方は Err<std::num::ParseIntError>
ですが、もう一方はコマンドライン引数がなかった時に None
になるので、いまは Err<String>
型の値を作って返しています。このままでは、両者を一つにまとめられないないので、リターン型の Result<i32, 何かのエラー型>
の 何かのエラー型
の部分を決められません。
ParseIntError
は、std::error::Error
トレイトを実装しています。そこで、コマンドライン引数がなかった時のエラー型を新たに定義して、同じように Error
トレイトを実装しましょう。こうすれば、両者を一つにまとめられます。
NotEnoughArgsError
という構造体を作ります。
use std::error;
use std::fmt;
#[derive(Debug)]
struct NotEnoughArgError {}
このように Debug
トレイトの実装を自動導出(derive)しています。Error
トレイトを実装する時は Debug
トレイトの実装が必須なので、自動導出するのがいいでしょう。
さらに、std::fmt::Display
トレイトが要求する fmt()
関数の実装も必要です。
impl fmt::Display for NotEnoughArgError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "引数が不足しています")
}
}
最後に std::error::Error
トレイトが要求する description()
関数と cause
関数を実装します。
impl error::Error for NotEnoughArgError {
fn description(&self) -> &str {
"引数が不足しています"
}
fn cause(&self) -> Option<&error::Error> {
None
}
}
これで NotEnoughArgsError
の実装は OK です。
さて、double_arg()
のリターン型を変更して Result<i32, error::Error>
にしたいところですが、これは Rust ではできません。Rust では関数の戻り値に、トレイトを使うことができないのです。
なぜなら、Rustは関数の引数と戻り値をコールスタックに直接積みますが、そのためには、コンパイル時に、これらの値が必要とするメモリのサイズが決まってないといけないのです。型を Error
にしてしまうと、Error
トレイトを実装している型ならどんなものでもスタックに置けることになります。しかし、個々の具象型(将来実装されるものも含む)によって、必要とするメモリのサイズが異なるので、それらをコンパイル時に列挙して、サイズを予測することは不可能です。
これを解決する方法のひとつはトレイトオブジェクトを使うことです。これは、元のエラー値を Box
で包むことで作れます。具体的には Box::new(エラー値)
とします。
rusti=> .type Box::new("hoge".parse::<i32>().unwrap_err())
... = Box<core::num::ParseIntError>
rusti=> .type Box::new("hoge".parse::<i32>().unwrap_err()) as Box<std::error::Error>
... = Box<std::error::Error>
値を box 化すると、その実体はスタックではなくて、ヒープに格納されるようになります。そしてスタックには、その実体へのポインタが積まれます。ポインタなら、必要なメモリサイズがコンパイル時に決まりますので大丈夫なわけです。
double_arg()
のリターン型を Result<i32, Box<error::Error>>
に変更します。
use std::env;
use std::error;
fn double_arg(mut argv: env::Args) -> Result<i32, Box<error::Error>> {
let number_str = try!(argv.nth(1).ok_or(NotEnoughArgsError));
let n = try!(number_str.parse::<i32>());
Ok(2 * n)
}
fn main() {
// main2.rs と同じ。省略
}
これで OK です。
あれ?でも、Err<NotEnoughArgError>
や Err<num::ParseIntError>
型の値を try!()
に渡しているのに、リターン型として Result<i32, Box<error::Error>>
と書けるのは不思議に思いませんか?さっき、box 化するには Box::new(エラー値)
とすると言ったばかりです。なぜ、それがなくてもコンパイルできるのでしょう。
種を明かすと try!()
マクロがエラーの型を変換しています。try!()
マクロは以下のように定義されています。
macro_rules! try {
($e:expr) => (match $e {
Ok(val) => val,
Err(err) => return Err(::std::convert::From::from(err)),
});
}
単に Err<なにかの型>
の値を返すのではなく、そこに格納されたエラーの値に std::convert::From
トレイトの from()
を適用してから、Err<E>
で包み直しています。この from()
関数は、ある型の値を、別の型の値へ変換します。そして、標準ライブラリには、以下のような From
実装があります。
impl<'a, E: Error + 'a> From<E> for Box<Error + 'a>
この実装により、Error
トレイトを実装した全ての型の値は、トレイトオブジェクト Box<Error>
へ変換できるのです。
つまり try!()
マクロは、以下の3つを抽象化しています。
- 場合分け
- 制御フロー
- エラー値の型変換
double_arg()
関数のコードを、もう一度比較してみましょう。
エラー処理なし(エラー時にクラッシュする)
fn double_arg(mut argv: env::Args) -> i32 {
let arg1 = argv.nth(1).unwrap(); // エラー1
let n = arg1.parse::<i32>().unwrap(); // エラー2
2 * n
}
エラー処理あり
fn double_arg(mut argv: env::Args) -> Result<i32, Box<error::Error>> {
let arg1 = try!(argv.nth(1).ok_or(NotEnoughArgsError));
let n = try!(arg1.parse::<i32>());
Ok(2 * n)
}
try!()
がエラーを変換してくれるので、以前の例にあった map()
が不要になりました。フル機能のエラー処理を行っていますが、そのためのオーバーヘッド(付随するコード)は、ほとんどありません!
Box<Error>
の些細な問題
前の節ではトレイトオブジェクト Box<Error>
を使いましたが、ここには些細な問題があります。Box
で包まれた値は Error
トレイトを実装していることまでは分かるのですが、その具象型(NotEnoughArgError
または ParseIntError
)がなんであったのかは、コンパイラにはわからなくなってしまうのです。このようなコンパイラーの動作を、型消去(type erasure) と呼びます。
例えば、呼び出し元の main()
関数の側で、エラーの種類に応じて処理を分岐させたくても、単純な場合分けでは判断できなくなります。以下のように書くことはできません。
fn main() {
match double_arg(env::args()) {
Ok(n) => println!("{}", n),
Err(Box(num::ParseIntError(..))) => {
println!("不正な数字です \"{}\"", env::args().nth(1).unwrap())
},
Err(err) => println!("エラー:{}", err),
}
}
方法はなくはありせん。実行時のリフレクションを使って、具象型へダウンキャストすることは可能です。
fn main() {
match double_arg(env::args()) {
Ok(n) => println!("{}", n),
Err(err) => {
match err.downcast_ref::<num::ParseIntError>() {
None => println!("エラー:{}", err),
Some(..) => println!("不正な数字です \"{}\"", env::args().nth(1).unwrap()),
}
}
}
}
起こりうるエラーの型一つ一つについて、ダウンキャストできたかの場合分けをしていくのは、少し冗長です。
ダウンキャストしなかったとしても、String
を返すよりは良くなっています。なぜなら、Error
トレイトに用意された、description()
メソッドや cause()
メソッドで、エラーについての、より詳細な情報を取得できるからです。
列挙型で独自のエラー型を定義する
この記事の締めくくりとして、トレイトオブジェクトを使った時の些細な問題を解決しましょう。2種類のエラーの値を包むことができる列挙型を定義して、Error
トレイトと From
トレイトを実装するのです。列挙型なら、トレイトオブジェクトと違い、型消去が起こりません。
列挙型を定義しましょう。コマンドラインインターフェイス(CLI)で起こるエラーということで、名前は CliError
としています。バリアントとして、NotEnoughArgs
と Parse(num::ParseIntError)
を用意します。
use std::error;
use std::fmt;
use std::num;
#[derive(Debug)]
enum CliError {
NotEnoughArgs,
Parse(num::ParseIntError),
}
Display
トレイトと、Error
トレイトを実装します。このように値に応じて、単純に処理を切り替えれば OK です。
impl fmt::Display for CliError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
CliError::NotEnoughArgs => write!(f, "引数が不足しています"),
CliError::Parse(ref err) => write!(f, "Parse error: {}", err),
}
}
}
impl error::Error for CliError {
fn description(&self) -> &str {
match *self {
CliError::NotEnoughArgs => "引数が不足しています",
CliError::Parse(ref err) => err.description(),
}
}
fn cause(&self) -> Option<&error::Error> {
match *self {
CliError::NotEnoughArgs => None,
CliError::Parse(ref err) => Some(err),
}
}
}
引数がなかった時のエラーは CliError
だけで十分表現できるので、前の節で作った NotEnhoughArgsError
は、いらなくなってしまいました。
double_arg()
関数を修正しましょう。
fn double_arg(mut argv: env::Args) -> Result<i32, CliError> {
let arg1 = try!(argv.nth(1).ok_or(CliError::NotEnoughArgs));
let n = try!(arg1.parse::<i32>().map_err(|e| CliError::Parse(e)));
Ok(2 * n)
}
このように、戻り値を Result<i32, CliError>
に変更しました。しかし、arg1.parse::<i32>()
が num::ParseIntError
を返したときのために、map_err()
コンビネータによる変換が必要になってしまいました。
これを取り除くのは簡単です。num::ParseIntError
から CliError
への変換をする From
トレイトを実装すればいいのです。
impl From<num::ParseIntError> for CliError {
fn from(err: num::ParseIntError) -> CliError {
CliError::Parse(err)
}
}
double_arg()
関数は最終的にこうなります。
fn double_arg(mut argv: env::Args) -> Result<i32, CliError> {
let arg1 = try!(argv.nth(1).ok_or(CliError::NotEnoughArgs));
let n = try!(arg1.parse::<i32>());
Ok(2 * n)
}
main()
関数は以前のままでも動きますが、せっかく列挙型を導入したので、double_arg()
関数の戻り値を使って、CliError::Parse()
の時も日本語のメッセージを出すようにしましょう。
fn main() {
match double_arg(env::args()) {
Ok(n) => println!("{}", n),
Err(err @ CliError::NotEnoughArgs) => println!("エラー:{}", err),
Err(CliError::Parse(..)) => {
println!("エラー:不正な数字です \"{}\"",
env::args().nth(1).unwrap())
}
}
}
これで完成です!
まとめ
-
Option<T>
とResult<T, E>
のコンビネータについて、よく学んでおきましょう。これらの値を簡潔に処理するための道具が用意されています。特によく使うのはand_then()
、map()
、unwrap_or()
です。 -
try!()
マクロを使いこなしましょう。このマクロは3つのことを抽象化してくれます:- 場合分け
- 制御フロー(早期リターン)
- エラー値の型変換
- エラー型に文字列を使うのは避けて、
Box<Error>
を使いましょう。必要に応じて、構造体によるエラー型を定義しましょう。 -
Box<Error>
の型消去が問題になる場合は、列挙型を定義してエラーの値をラップしましょう。
3と4は必須ではありません。簡単なアプリケーションを書いている時は、Result<T, String>
型で十分なこともあります。
この記事で解説した内容を、もっと詳しく知りたくなったら、公式ドキュメント(日本語翻訳版)の エラーハンドリング を読んでください。
もちろん、公式ドキュメントのそれ以外の章も、Rust を使う上で理解しておくべき内容が詰まっていますので、ぜひ読んでもらいたいです。翻訳の進捗状況と、翻訳済みページヘのリンクは ここ にあります。