Rustのエラーハンドリングの基本
普段仕事ではRubyを使ってWebアプリケーションの開発をしているのですが、Rustに興味があり、勉強をしています。
Rustのエラーハンドリングについて勉強したのでまとめてみました。
Rustのエラーハンドリング
多くのプログラミング言語にはエラーハンドリングに例外や戻り値を利用しますが、Rustでは戻り値を使ってエラーハンドリングをします。
Rustのエラーハンドリングは大きく分けて2つの種類があり、リカバリーできるエラーとリカバリーできないエラーであります。
リカバリーできるエラーはResult<T, E>
値があり、リカバリーできないエラーはpanic!
マクロなどがあります。
リカバリーできないエラーはプログラムの実行を中止するが、多くのエラーはプログラムを完全にストップさせるほどの必要はないため、基本的にはResult
型を使ってエラーハンドリングします。
Result型
Rustには例外がないが、代わりに失敗する可能性のある関数はResult
型を返してエラーの場合を対処します。
Result
型は下記のようなenumとしてOk
とErr
の2列からなるよう定義されています。
enum Rusult<T, E> { Ok(T), Err(E)
Ok(T)
は成功結果でErr(E)
はエラー結果を返します。
T
とE
はジェネリックな型引数を表しており、T
が成功したときに返される値の型を表し、E
が失敗したときに返される型を表します。
Result型の処理方法
Result型の処理方法の一つはmatch式を使い、関数がResult
型を返したときにパターンマッチさせます。
use std::num::ParseIntError; fn parse_str(num_str: &str) -> Result<u32, ParseIntError> { num_str.parse::<u32>() } fn main() { let num_str = "3"; match parse_str(num_str) { Ok(num) => println!("{}", num), Err(err) => eprintln!("{}", err), } let num_str = "str"; match parse_str(num_str) { Ok(num) => println!("{}", num), Err(err) => eprintln!("{}", err), } }
上記を実行すると下記のように出力されます。
3 invalid digit found in string
parse_str
へ引数に渡された文字列により、その処理の結果に対してのResult型が返されます。その返された結果をmatch
式でパターンマッチさせ、返された結果がOk
かErr
によりそれぞれの結果を処理します。
また、match
式を使う以外の別の方法としてResult
型に用意されているメソッドがあるので、こちらを使って処理することができます。
use std::num::ParseIntError; fn parse_str(num_str: &str) -> Result<u32, ParseIntError> { num_str.parse::<u32>() } fn main() { let num_str = "3"; parse_str(num_str) .map(|num| println!("{}", num)) .unwrap_or_else(|err| eprintln!("{}", err)); let num_str = "str"; parse_str(num_str) .map(|num| println!("{}", num)) .unwrap_or_else(|err| eprintln!("{}", err)); }
実行結果はmatch
式を使ったコードと同じ結果となります。
map
はResult型の値がOk
時に、map
の引数に与えられた関数かクロージャをOk
の中の値に対して実行しその結果をOk
の中身として返し、Err
の場合はそのまま値を返します。
unwrap_or_else
はOk
の場合はそのまま値を返し、Err
のときは関数かクロージャを実行してその結果を返します。
?演算子
エラーハンドリングするためにすべてのエラーをその場で処理してmatch
式を記載する必要はない。Rustでは?
演算子を用いてエラーハンドリングを呼び出し元に任せることができます。
例えば、下記のコードのようなコードの場合、parse_two_str
メソッド内でparse
メソッドを2回呼び出しているが、それぞれのエラーハンドリングはparse_two_st
の呼び出し元に任せることができます。
use std::num::ParseIntError; fn parse_two_str(first_num_str: &str, second_num_str: &str) -> Result<(u32, u32), ParseIntError> { let first_num = first_num_str.parse::<u32>()?; let second_num = second_num_str.parse::<u32>()?; Ok((first_num, second_num)) } fn main() { let first_num_str = "3"; let second_num_str = "5"; match parse_two_str(first_num_str, second_num_str) { Ok((first_num, second_num)) => println!("{}, {}", first_num, second_num), Err(err) => eprintln!("{}", err), } let first_num_str = "str"; let second_num_str = "5"; match parse_two_str(first_num_str, second_num_str) { Ok((first_num, second_num)) => println!("{}, {}", first_num, second_num), Err(err) => eprintln!("{}", err), } }
実行結果は下記のような結果になります。
3, 5 invalid digit found in string
関数parse_two_str
は与えられた引数それぞれに対してparse
メソッドを実行しており、Result型が返されます。?
演算子を使用することでOk
が返された場合にはResult
型を解いてOk
列挙列の中身を取り出し、プログラムを継続する。Err
が返された場合は呼び出し元にErr
値を即時にリターンします。
?
演算子と同じような動作をmatch
式と使って実装できます。
fn parse_two_str(first_num_str: &str, second_num_str: &str) -> Result<(u32, u32), ParseIntError> { let first_num = match first_num_str.parse::<u32>() { Ok(first_num) => first_num, Err(err) => return Err(err), }; let second_num = match second_num_str.parse::<u32>() { Ok(second_num) => second_num, Err(err) => return Err(err), }; Ok((first_num, second_num)) }
異なる種類のエラーの扱い
上記のparse_str
を修正して標準入力から入力した文字をparse
するメソッドに変更したいとします。
その場合、標準入力を読み取るためにstdin
を使用するが、ただ追加しただけだと下記のようになります。
use std::num::ParseIntError; use std::io; fn parse_str() -> Result<u32, ParseIntError> { let mut buffer = String::new(); io::stdin().read_line(&mut buffer)?; let num = buffer.trim().parse::<u32>()?; Ok(num) } fn main() { match parse_str() { Ok(first_num) => println!("{}", first_num), Err(err) => eprintln!("{}", err), } }
このコードはコンパイルに失敗します。なぜならio::stdin().read_line(&mut buffer)
のErr
の型がParseIntError
と違うからです。read_line
のErr
の型はstd::io::Error
です。
?
演算子の作用でErr
型を定義された関数の戻り値(今回の場合、ParseIntError
)のErr
型に変換しようとするが、ParseIntError
型への変換は定義されていないため、コンパイルエラーとなります。
これを解決する方法は、関数のErr
型の戻り値の定義をトレイドオブジェクトで定義すると解決します。定義はBox<dyn std::error::Error>
と書きます。標準ライブラリーのエラーはすべてError
トレイトを実装しているため、トレイトオブジェクトで定義すると変換できます。
修正したコードは下記になります。これでコンパイルができるようになります。
use std::io; fn parse_str() -> Result<u32, Box<dyn std::error::Error>> { let mut buffer = String::new(); io::stdin().read_line(&mut buffer)?; let num = buffer.trim().parse::<u32>()?; Ok(num) } fn main() { match parse_str() { Ok(first_num) => println!("{}", first_num), Err(err) => eprintln!("{}", err), } }
まとめ
Rustのエラーハンドリングについて基本的な部分について勉強したことをまとめてみました。
実際にエラーハンドリングはfailure
というライブラリが使われるのが一般的みたいです。こちらについてもいずれまとめてみたいと思ってます。
参考文献
- The Rust Programming Language
- プログラミング言語Rust
- プログラミングRust Jim Blandy (著), Jason Orendorff (著), 中田 秀基 (翻訳)