RustのResultでエラーハンドリング処理を行う

618, 2023-01-06

目次

RustのResultでエラーハンドリングをする

Rustでエラー処理をしたい場合によく使われるのがResultを使ったエラーハンドリングです。
Resultは正常時の返り値とエラー時の返り値を1つにまとめたものです。
関数が正常終了したら正常時の返り値を、エラーになったらエラー時の返り値を返します。

その関数を呼び出した側はmatch演算子などで返り値をチェックし、エラー時の処理などを記述します。

この記事ではRustのResultを使ったエラーハンドリングを解説します。

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>となっているところに注目してください。
ResultResult<正常時の返り値の型, エラー時の返り値の型>になります。
ですのでこの場合は正常時は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になります。

このようにResultErr()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()?なども使いこなして快適なエラーハンドリングを行ってください。

(^ _ ^)

エラー処理こそ

(・ v ・)

プログラムの神髄



この記事のアンケートを送信する