Rustのエラーハンドリングの基本

普段仕事ではRubyを使ってWebアプリケーションの開発をしているのですが、Rustに興味があり、勉強をしています。
Rustのエラーハンドリングについて勉強したのでまとめてみました。

Rustのエラーハンドリング

多くのプログラミング言語にはエラーハンドリングに例外や戻り値を利用しますが、Rustでは戻り値を使ってエラーハンドリングをします。
Rustのエラーハンドリングは大きく分けて2つの種類があり、リカバリーできるエラーとリカバリーできないエラーであります。 リカバリーできるエラーはResult<T, E>値があり、リカバリーできないエラーはpanic!マクロなどがあります。 リカバリーできないエラーはプログラムの実行を中止するが、多くのエラーはプログラムを完全にストップさせるほどの必要はないため、基本的にはResult型を使ってエラーハンドリングします。

Result型

Rustには例外がないが、代わりに失敗する可能性のある関数はResult型を返してエラーの場合を対処します。 Result型は下記のようなenumとしてOkErrの2列からなるよう定義されています。

enum Rusult<T, E> {
  Ok(T),
  Err(E)

Ok(T)は成功結果でErr(E)はエラー結果を返します。
TEジェネリックな型引数を表しており、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式でパターンマッチさせ、返された結果がOkErrによりそれぞれの結果を処理します。
また、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_elseOkの場合はそのまま値を返し、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_lineErrの型は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というライブラリが使われるのが一般的みたいです。こちらについてもいずれまとめてみたいと思ってます。

参考文献