RustのResultでエラーハンドリング処理を行う
- 作成日: 2023-01-05
- 更新日: 2023-12-24
- カテゴリ: Rust
RustのResultでエラーハンドリングをする
Rustでエラー処理をしたい場合によく使われるのがResult
を使ったエラーハンドリングです。
Result
は正常時の返り値とエラー時の返り値を1つにまとめたものです。
関数が正常終了したら正常時の返り値を、エラーになったらエラー時の返り値を返します。
その関数を呼び出した側はmatch
演算子などで返り値をチェックし、エラー時の処理などを記述します。
この記事ではRustのResult
を使ったエラーハンドリングを解説します。
関連記事
RustでJSONの読み書きを行う
RustでTCPクライアント/サーバーを作る【ソケット通信】
Rustでファイルサイズを取得する方法【std::fs::metadata】
RustのBoxの使い方【ヒープにメモリを確保!初心者向け】
RustのResultでエラーハンドリング処理を行う
RustのThread(スレッド)で並行処理をする方法
RustのVecの使い方【vec!, push, pop】
Rustのassert!の使い方【assert!, assert_eq!, assert_ne!】
Resultを使った基本的なコード
Result
を使った基本的なコードは↓になります。
// 配列に偶数が含まれていたらエラーを発生させる
fn check_even_digits(ary: &[i32]) -> Result<i32, &str> {
for x in ary {
if x % 2 == 0 {
return Err("found even number")
}
}
Ok(1)
}
fn test_result() {
let result = check_even_digits(&[1, 3, 2]);
match result {
Err(err) => eprintln!("{}", err),
Ok(ok) => println!("{}", ok),
}
let result = check_even_digits(&[1, 3, 5]);
match result {
Err(err) => eprintln!("{}", err),
Ok(ok) => println!("{}", ok),
}
}
上記のコードを実行すると↓の結果になります。
found even number
1
check_even_digits()
関数は引数の配列をチェックして偶数が見つかったらエラー文字列を返し、見つからなかったら整数の1
を返します。
関数の返り値の型がResult<i32, &str>
となっているところに注目してください。
Result
はResult<正常時の返り値の型, エラー時の返り値の型>
になります。
ですのでこの場合は正常時はi32
を返し、エラー時は&str
を返すということになります。
Result
を返す関数では↓の2つが使えます。
- Err
- Ok
Err()
はエラーを返すために使い、Ok()
は正常時の返り値を返すために使います。
上記のコードではreturn Err("found event number")
」というところでエラーを返しています。
正常時の返り値は「Ok(1)
」で返しています。
Result
を受け取る関数の呼び出し側では
let result = check_even_digits(&[1, 3, 2]);
match result {
Err(err) => eprintln!("{}", err),
Ok(ok) => println!("{}", ok),
}
という風にmatch
演算子でエラーハンドリングをしています。
↑の場合、Err()
とOk()
を使って変数result
からエラーメッセージと整数を取り出しています。
つまりerr
が「found event number
」の文字列で、ok
が整数の1
になります。
このようにResult
はErr()
とOk()
を使ってエラーハンドリングします。
これはunwrap()
やexpect()
、?
で省略することもできます。
unwrapを使ったエラー処理の省略
先ほどのコードではエラーメッセージをErr()
で抽出してハンドリングしてました。
これはunwrap()
で省略することもできます。
fn test_result() {
let ok = check_even_digits(&[1, 3, 5]).unwrap();
println!("{}", ok);
let ok = check_even_digits(&[1, 3, 2]).unwrap();
println!("{}", ok);
}
↑のコードを実行すると↓の結果になります。
1
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: "found even number"', src/main.rs:28:44
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
unwrap()
はResult
でエラーが返ってきた時にパニックを起こします。
パニックではエラーの値を出力してプログラムを中断します。
expectを使ったエラー処理の省略
unwrap()
ではエラー発生時のメッセージを呼び出し側で指定できませんでした。
expect()
は指定できます。
let ok = check_even_digits(&[1, 3, 5]).expect("failed");
println!("{}", ok);
let ok = check_even_digits(&[1, 3, 2]).expect("failed");
println!("{}", ok);
↑のコードを実行すると↓の結果になります。
1
thread 'main' panicked at 'failed: "found even number"', src/main.rs:34:44
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
expect("failed")
でエラー発生時に「failed」というメッセージを表示するようにしています。
↑を見ると「panicked at 'failed
」になっていますね。
もちろんエラーの値も出力されています。
エラー委譲演算子「?」でエラー処理を省略する
Result
のエラーハンドリングを真面目に書いているとあっという間にコードが肥大化してC言語のようになってしまいます。
Rust書いてるんだからもっと楽したいわけですが、そういう時はエラー委譲演算子が使えます。
エラー委譲演算子は「?
」になります。
↓のコードを見てください。
fn check_even_digits_wrapper(ary: &[i32]) -> Result<i32, &str> {
// エラーが発生した場合は「?」を使ってエラーをそのまま上に投げる
let ok = check_even_digits(&ary)?;
println!("wrapper: {}", ok);
Ok(ok)
}
fn test_denpan() {
let result = check_even_digits_wrapper(&[1, 3, 3]);
match result {
Err(err) => eprintln!("{}", err),
Ok(ok) => println!("{}", ok),
}
println!("----");
let result = check_even_digits_wrapper(&[1, 2, 3]);
match result {
Err(err) => eprintln!("{}", err),
Ok(ok) => println!("{}", ok),
}
}
↑のコードを実行すると↓の結果になります。
wrapper: 1
1
----
found even number
Result
を使った関数内では「?」演算子を使えます。
この演算子をResult
を返す関数の呼び出しに使うと、エラーが発生したときにエラーを上に投げてくれます。
つまりcheck_even_digits(&ary)?;
で関数を呼び出してますがエラーが発生した場合はこの段階で勝手にreturn
してくれて、エラーを上に伝搬してくれます。
ですので関数呼び出し側は正常時の返り値だけを処理するだけで済みます。
check_even_digits_wrapper()
を見るとそのようになっていますよね。
もちろんtest_denpan()
の関数内では伝搬されてきたエラーは処理しないといけません。
ですのでmatch
演算子できっちりエラー処理するかあるいはunwrap()
などを使ってパニックを使うかになります。
独自エラーでエラー処理をする
さきほどまでのコードはエラーをエラーメッセージで表現、つまり文字列の&str
で表現してました。
このエラーはもちろんenum
を指定することもできます。
enum
を使って独自エラーを定義したい場合などは↓のようなコードを書きます。
// 独自エラー
#[derive(Debug)]
enum OddError {
FoundOdd,
}
// OddError::FoundOdd が println!() で参照されたときに出力する文字列を定義する
impl std::fmt::Display for OddError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
OddError::FoundOdd => write!(f, "found odd number"),
}
}
}
// 配列に奇数が含まれていたらエラーを発生させる
fn check_odd_digits(ary: &[i32]) -> Result<&str, OddError> {
for x in ary {
if x % 2 == 1 {
return Err(OddError::FoundOdd); // <- ここで独自エラーを使ってる
}
}
Ok("all green")
}
fn test_even_error() {
let result = check_odd_digits(&[2, 4, 5]);
match result {
Err(err) => eprintln!("{}", err),
Ok(ok) => println!("{}", ok),
}
let result = check_odd_digits(&[2, 4, 6]);
match result {
Err(err) => eprintln!("{}", err),
Ok(ok) => println!("{}", ok),
}
}
↑のコードを実行すると↓の結果になります。
found odd number
all green
enum
を使うとエラー処理でマジックナンバーを使わなくて済むというメリットがあります。
ですが変数などを付与したい場合は構造体などを使う必要が出てきます。
独自エラー構造体を作る
独自エラーの構造体は↓のように作ります。
// 独自エラー構造体
#[derive(Debug, Clone)]
struct BadError {
detail: String,
}
impl std::fmt::Display for BadError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "bad error. {}", self.detail)
}
}
// これは他のエラーがBadErrorをラップするときに重要です
impl std::error::Error for BadError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
}
impl BadError {
// detailはユーザー定義の情報
fn new(detail: &str) -> Self {
BadError {
detail: String::from(detail),
}
}
}
fn check_good(n: i32) -> Result<i32, BadError> {
if n < 0 {
Err(BadError::new("invalid number"))
} else {
Ok(n)
}
}
fn test_bad_error() {
let result = check_good(-1);
match result {
Err(err) => eprintln!("{}", err),
Ok(ok) => println!("{}", ok),
}
}
↑のコードを実行すると↓の結果になります。
bad error. invalid number
独自エラーを定義するだけなのにコードが長くなってしまって大変ですね。
これを解消するための外部ライブラリはthiserror
などがあります。
興味がある方は調べてみてください。
おわりに
RustのエラーハンドリングはResult
を使う方法を解説しました。
unwrap()
や?
なども使いこなして快適なエラーハンドリングを行ってください。
🦝 < エラー処理こそ
🐭 < プログラムの神髄