エラー・ハンドリングのキホン

今日の起点となるコードはこれにしよう。

fn parse_string(s: &str) -> Result<u32, std::num::ParseIntError> {
    s.parse::<u32>()
}

fn main() {
    println!("{:?}", parse_string("-1")); //Output: Err(ParseIntError { kind: InvalidDigit })
}

parse_string() 関数の返り値の型 Result は列挙型 enum1,以下の2つの列挙子(variant)で構成されている。

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Result など列挙型の評価には match 式を使う。 たとえば parse() に失敗した際に 0 をセットしたいなら

fn main() {
    let n = match parse_string("-1") {
        Ok(x) => x,
        Err(_) => 0,
    };
    println!("{}", n); //Output: 0
}

などと書くことができる。

Result 型を使ったエラー・ハンドリング

このように Result 型を使うことで基本的なエラー・ハンドリングが可能になる。 いくつか例を挙げていこう。

Panic を投げる

parse_string() 関数が Err を返した場合に強制終了したいのであれば

fn main() {
    let n = match parse_string("-1") {
        Ok(x) => x,
        Err(e) => panic!(e), //Output: thread 'main' panicked at 'Box<Any>', src/main.rs:8:19
    };
    println!("{}", n); //do not reach
}

panic! マクロで panic を投げればよい。 ちなみに,環境変数 RUST_BACKTRACE1 をセットすると panic 時にスタックトレース情報も吐く。

単に panic を投げればいいのであれば Result::unwrap() メソッドを使えば上述のコードとほぼ同じ結果が得られる。

fn main() {
    let n = parse_string("-1").unwrap(); //Output: thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }', src/main.rs:6:13
    println!("{}", n); //do not reach
}

Panic を投げる際のメッセージを指定するには Result::expect() メソッドを使う。

fn main() {
    let n = parse_string("-1").expect("error in parse_string() function"); //Output: thread 'main' panicked at 'error in parse_string() function: ParseIntError { kind: InvalidDigit }', src/main.rs:6:13
    println!("{}", n); //do not reach
}

Panic 以外のハンドリング

エラー時に単に panic を投げるのではなく,何らかの処理を行って普通にプロセスを終了したいのであれば Result::unwrap_or_else() メソッドを使って

fn main() {
    let n = parse_string("-1").unwrap_or_else(|e| {
        println!("Error in parse_string() function: {:?}", e); //Output: Error in parse_string() function: ParseIntError { kind: InvalidDigit }
        std::process::exit(1);
    });
    println!("{}", n); //do not reach
}

などとすることもできる。 プロセスを終了するのではなく,最初の例のように解析失敗時に 0 をセットしたいなら

fn main() {
    let n = parse_string("-1").unwrap_or_else(|_| 0);
    println!("{}", n); //Output: 0
}

てな感じにも書ける。 あるいはもっと簡単に Result::unwrap_or() メソッドを使って

fn main() {
    let n = parse_string("-1").unwrap_or(0);
    println!("{}", n); //Output: 0
}

とも書ける。

エラーの委譲

次に以下の関数を考える。 2つの数文字列を与えて組(tuple)にして返す。

fn parse_pair_strings(s1: &str, s2: &str) -> Result<(u32, u32), std::num::ParseIntError> {
    let x = match s1.parse::<u32>() {
        Ok(n) => n,
        Err(e) => return Err(e),
    };
    let y = match s2.parse::<u32>() {
        Ok(n) => n,
        Err(e) => return Err(e),
    };
    Ok((x, y))
}

fn main() {
    println!("{:?}", parse_pair_strings("1", "-1")); //Output: Err(ParseIntError { kind: InvalidDigit })
}

parse_pair_strings() の仮引数 s1 および s2 に対してそれぞれ parse() を行うのだが,返り値が Err の際にはエラーをそのまま返して呼び出し元にハンドリングを委譲している。

返り値の型が同じ Result なら ? 演算子を使ってエラーの委譲をもっと簡単に書くことができる。 こんな感じ。

fn parse_pair_strings(s1: &str, s2: &str) -> Result<(u32, u32), std::num::ParseIntError> {
    Ok((s1.parse::<u32>()?, s2.parse::<u32>()?))
}

すっきりー!

エラーの汎化

今度は,引数に文字列を渡すのではなく,標準入力から文字列を取得して parse() してみよう。

fn parse_from_stdin() -> Result<u32, std::num::ParseIntError> {
    let mut buf = String::new();
    std::io::stdin().read_line(&mut buf)?; //Compile error: `?` couldn't convert the error to `std::num::ParseIntError`
    buf.trim().parse::<u32>()
}

当然ながらこれはコンパイルエラーになる。 何故なら read_line() 関数では Err の値が std::io::Error 型になるので std::num::ParseIntError 型とはマッチしないためだ。

これを解決するには std::error::Error 型を汎化型として使えばよい。

std::error::Error 型を使う際は Box<dyn Trait> にするようだ。 こんな感じ。

fn parse_from_stdin() -> Result<u32, Box<dyn std::error::Error>> {
    let mut buf = String::new();
    std::io::stdin().read_line(&mut buf)?;
    let n = buf.trim().parse::<u32>()?;
    Ok(n)
}

fn main() {
    println!("{:?}", parse_from_stdin());
}

実行するとこんな感じになる。

$ echo -1 | cargo run
Err(ParseIntError { kind: InvalidDigit })

main() 関数側でもう少し細かくエラーを見てみよう。 こんな感じかなぁ。

fn main() {
    let n = match parse_from_stdin() {
        Ok(x) => x,
        Err(err) => {
            match err.downcast_ref::<std::io::Error>() {
                Some(e) => {
                    println!("io::Error in parse_from_stdin(): {}", e);
                    std::process::exit(1);
                }
                _ => (),
            };
            match err.downcast_ref::<std::num::ParseIntError>() {
                Some(e) => {
                    println!("ParseIntError in parse_from_stdin(): {}", e);
                    std::process::exit(1);
                }
                _ => (),
            };
            println!("Other error in parse_from_stdin(): {}", err);
            std::process::exit(1);
        }
    };
    println!("{}", n);
}

こうすれば Box<dyn std::error::Error> 型からもとのエラー型を抽出して個別に処理できそう。 これを実行するとこんな感じ。

$ echo -1 | cargo run
ParseIntError in parse_from_stdin(): invalid digit found in string

んー。 かなり面倒くさいな。 もう少しマシな戦略を探すべきか。

Option 型を使ったエラー・ハンドリング

上述のコードにある downcast_ref() メソッドは Option 型の値を返す。 Option 型も列挙型で,以下の2つの列挙子で構成されている。

enum Option<T> {
    Some(T),
    None,
}

None はいわゆる Null 値,つまり値がないことを示す。 関数の実行結果が Null 値を取り得る場合に Option 型を返すことで,呼び出し元に Null 値のハンドリングを促すことができる。

起点のコードはこんな感じでどうだろうか。

fn main() {
    let nihongo = "日本語";
    for i in 0..4 {
        println!("{}: {:?}", i, nihongo.chars().nth(i));
    }
}

実行するとこんな感じになる。

$ cargo run
0: Some('日')
1: Some('本')
2: Some('語')
3: None

Panic を投げる

結果が None なら panic を投げるようにしてみる。

fn main() {
    let nihongo = "日本語";
    for i in 0..4 {
        let ch = match nihongo.chars().nth(i) {
            Some(c) => c,
            None => panic!("Out of bounds"),
        };
        println!("{}: {}", i, ch)
    }
}

実行するとこんな感じ。

$ cargo run
0: 日
1: 本
2: 語
thread 'main' panicked at 'Out of bounds', src/main.rs:6:21

また Option 型にも unwrap()expect() といったメソッドがあり, None が返ったら panic を投げる。

fn main() {
    let nihongo = "日本語";
    for i in 0..4 {
        println!("{}: {}", i, nihongo.chars().nth(i).expect("Out of bounds"));
    }
}
$ cargo run
0: 日
1: 本
2: 語
thread 'main' panicked at 'Out of bounds', src/main.rs:4:31

Panic 以外のハンドリング

たとえば,こんな感じかな。

fn main() {
    let nihongo = "日本語";
    for i in 0..4 {
        let ch = match nihongo.chars().nth(i) {
            Some(c) => c,
            None => break,
        };
        println!("{}: {}", i, ch)
    }
}
$ cargo run
0: 日
1: 本
2: 語

まぁイテレータやコレクションでこんな書き方する人はおらんじゃろうけど。 あンまりいい例示じゃなくてすまん。

やっぱ例外処理は要らんよね

Rust の列挙型は(C/C++ や Java などと異なり)型の列挙を行い,パターン・マッチングによって処理を振り分ける。 この仕組みを上手く使ってエラー・ハンドリングを行っているわけだ。

こうやってみると,やっぱ例外処理は要らんよね(笑)

ブックマーク

参考図書

photo
プログラミング言語Rust 公式ガイド
Steve Klabnik (著), Carol Nichols (著), 尾崎 亮太 (翻訳)
KADOKAWA 2019-06-28 (Release 2019-06-28)
単行本
4048930702 (ASIN), 9784048930703 (EAN), 4048930702 (ISBN)
評価     

公式ドキュメントの日本語版。索引がちゃんとしているので,紙の本を買っておいて手元に置いておくのが吉。

reviewed by Spiegel on 2020-02-24 (powered by PA-APIv5)

photo
プログラミングRust
Jim Blandy (著), Jason Orendorff (著), 中田 秀基 (翻訳)
オライリージャパン 2018-08-10
単行本(ソフトカバー)
4873118557 (ASIN), 9784873118550 (EAN), 4873118557 (ISBN)
評価     

Eブック版あり。公式ドキュメントよりも系統的に書かれているので痒いところに手が届く感じ。ただし量が多いので,一度斜め読みしたらあとは傍らに置いて必要に応じてつまみ食いしていくのがいいだろう。

reviewed by Spiegel on 2020-03-08 (powered by PA-APIv5)


  1. 列挙型については「Rust の型に関する覚え書き」を参照のこと。 ↩︎