Rustのエラーハンドリングガイド!Option型やResult型を使いこなす

Rustでは、他の言語のようにnullや例外(Exception)が存在しません。
その代わりに、Rustは「値の存在有無をOption型」、「処理の成功・失敗をResult型」で扱います。また、回復不能なエラーが発生した場合にはpanic!でプログラムの実行を中止させることができます。

この記事では、Rustのエラーハンドリング方法をわかりやすく解説します。

1. 値の存在有無を表すOption<T>

Rustでは、値が存在するか存在しないかという概念をenumOption<T>型で定義しています。 Option<T>型は、Some<T>Noneの列挙型として定義されます。Some<T>は型Tの値を保持し、Noneは値がないことを表します。これは、値が存在しない可能性がある値をより安全に扱うために、Rustのコードで一般的に使用されます。

<T>ジェネリックスと呼ばれ任意のデータ型を表します。よくわからない場合は、ジェネリックなデータ型をご覧ください。

pub enum Option<T> {
    None,   // 値が存在しないことを表す
    Some(T) // `T`型の値が存在することを表す
}

Option<T>型はとても有益なので、列挙子のSomeNoneOption::という接頭辞なしに利用できます。

let some_number = Some(5);
let some_string = Some("a string");

let absent_number: Option<i32> = None;

Option<T>型はどういうときに使うか

値が必ずしも存在しない可能性がある場合は、Option<T>型が利用されます。

Option<T>で値があるときに処理をしたい場合は、match式と以下のように組み合わせることができる。

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
    }
}

fn main() {
    let five = Some(5);
    let six = plus_one(five); // Some(6)
    let none = plus_one(None); // None
}

また、if letで値があるときだけなにか処理をしたいという制御フローを簡潔に書くことができます。

fn main() {
    let some_u8_value = Some(0u8);

    // matchで書いたパターン
    match some_u8_value {
        Some(3) => println!("three"),
        _ => (),
    }

    // if let で書き換えたパターン
    if let Some(3) = some_u8_value {
        println!("three");
    }
}

Option<T>型のよく使う便利メソッド

unwrap()expect()で値を取得する

unwrapexpectを使うことでOption<T>の値がSomeの場合は値を取得し、Noneの場合はパニックを起こすことができます。値がないときにパニックが起きるので、テストコードやサンプルコードなどでよく使われがちです。本番コードではmatch?などでハンドリングしたほうがよいです。

fn main() {
    let some_number = Some(5);
    assert_eq!(some_number.unwrap(), 5);

    // let absent_number: Option<i32> = None;
    // absent_number.unwrap(); // 値がNoneなのでパニックが起きる

    let absent_number: Option<i32> = None;
    absent_number.unwrap(); // 値がNoneなのでパニックが起きる

    let absent_number: Option<i32> = None;
    absent_number.expect("custom panic message"); // 値がNoneなのでカスタムメッセージでパニックが起きる
}

?によるアンパックする

Optionの値を取得するためにmatchを使うこともできますが、?を使うほうがコードの可読性が高まります。?は評価した値がSomeの場合は値を取得し、Noneの場合は実行中の関数を終了させNoneを返します。制約として、関数がOption<T>を返す必要があります。

fn main() {
    assert_eq!(add_plus_one(Some(2)), Some(3));
    assert_eq!(add_plus_one(None), None);
}

fn add_plus_one(x: Option<i32>) -> Option<i32> {
    let v = x? + 1; // x? が Noneの場合は、ここで早期リターンされる
    Some(v)
}

take()で値を取り出す

take()で値を取り出し、Noneを設定します。

fn main() {
    let mut x = Some(2);
    // xから値を取り出しyに代入し、代わりにNoneにする
    let y = x.take();
    assert_eq!(x, None);
    assert_eq!(y, Some(2));


    let mut x: Option<u32> = None;
    // xから値を取り出しyに代入し、代わりにNoneにする
    let y = x.take();
    assert_eq!(x, None);
    assert_eq!(y, None);
}

mapで値を変換する

map()は値に関数を適用して、Option<T>Option<U> に変換することができます。メソッドチェインが必要なときにmapを使うことで、可読性を高めることができます。

fn main() {
    let some_string = Some("a string".to_string());

    // map は Option<T> から Option<U> への変換を行う
    let some_length = some_string.map(|s| s.len());
    assert_eq!(some_length, Some(8));

    // None の場合は Noneを返す
    let none_length: Option<usize> = None.map(|s: String| s.len());
    assert_eq!(none_length, None);
}

他にもOption<T>には便利なメソッドがあります。より詳しく知りたい場合は、Option in std::option - Rustを確認してみてください。

処理の成功・失敗を表すResult

Result<T, E>型は、Ok<T>Err<E>の2つ列挙子を持つ列挙型です。Okは、成功した操作を表す型Tの値を保持し、Errはエラーを表す型Eの値を保持します。この型は、成功するかどうか不確かな値を返す場合に役立ちます。

pub enum Result<T, E> {
    Ok(T),  // 成功した値が含まれる
    Err(E), // エラーの値が含まれる
}

Result型はどういうときに使うか

Resut<T, E>型では、処理が成功したか失敗したかといったエラーハンドリングをします。 Resut<T, E>を使うことで、例外を起こさずにデータ型にもとづいてエラーハンドリングができるので、エラーハンドリングが実装されているかどうかをコンパイラにチェックさせることができます。

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");

    let _f = match f {
        // ファイルがある場合は、そのファイルを返す
        Ok(file) => file,
        // ファイルがない場合は、パニックを起こす
        Err(error) => {
            panic!("There was a problem opening the file: {:?}", error);
        }
    };
}

Result型のよく使う便利メソッド

Result型にも、Option<T>型と同じような便利なメソッドが定義されています。

?で値を取得する

?演算子は、Resultの値がOkの場合はOkの中身の値が返ってきます。一方、Resultの値がErrの場合は早期リターンをしてErrを返します。
?演算子を使うことで、エラーハンドリングのコードが読みやすくなります。?演算子の制約としては、戻り値にResult<T, E>を持つ関数やメソッドでしか利用できません。

use std::fs::File;
use std::io;
use std::io::Read;

fn main() {
    let s = read_username_from_file();
    match s {
        Ok(s) => println!("s is {}", s),
        Err(e) => println!("e is {:?}", e),
    }
}

fn read_username_from_file() -> Result<String, io::Error> {
    let mut f = File::open("hello.txt")?; // ファイルがない場合早期リターンでErrを返す
    let mut s = String::new();
    f.read_to_string(&mut s)?; // ファイルの読み込みに失敗した場合、早期リターンでErrを返す
    Ok(s)
}

unwrap()expect()で値を取得する

unwrapexpectを使うことでResult<T, E>の値がOk(T)の場合はT型の値を取得し、Err<E>の場合はパニックを起こすことができます。値がないときにパニックが起きるので、テストコードやサンプルコードなどでよく使われがちです。本番コードではmatch?などでエラーハンドリングをしたほうがよいです。

fn main() {
    // ファイルがない場合はパニックが呼ばれ、ファイルがあればファイルが返される
    let f = File::open("hello.txt").unwrap();

    // ファイルがない場合は"Failed to open hello.txt"というメッセージでパニックが表示され、
    // ファイルがあればファイルが返される
    let f = File::open("hello.txt").expect("Failed to open hello.txt");
}

mapで値をマップする

map()は値に関数を適用して、Result<T, E>Result<U, E> に変換します。値がErrの場合は関数は適用されません。

fn main() {
    // Okなので、値が2倍されて"200"が出力される
    match "100".parse::<i32>().map(|i| i * 2) {
        Ok(n) => println!("{n}"),
        Err(..) => println!("parse error"),
    }

    // Errなので、"parse error"が出力される
    match "invalid".parse::<i32>().map(|i| i * 2) {
        Ok(n) => println!("{n}"),
        Err(..) => println!("parse error"),
    }
}

Result<T, E>型のより詳しいメソッドを知りたい場合は、Result in std::result - Rustを眺めてみてください。

実行を中止させるpanic!

panic!マクロを使うことで、現在のスレッドをパニックにし、プログラムはすぐに終了します。

fn main() {
    panic!("crash and burn"); // ここでプログラムが終了する

    println!("Do something");
}

panic!はどういうときに使うか

サンプルコードやテストで条件をアサートするのに利用すると便利です。特に、これらのケースでのパニックはunwrap()expect()で利用されることが多いでしょう。

また、panic!はプログラムで検出された回復不能なバグを表すエラーを作成されるために使用することもあります。

Rustのエラーハンドリングまとめ

Rustのエラーハンドリングの機能のまとめです。

  • RustにはNullや例外(Exception)の機能がない。
  • Nullの代わりに、値があるなしはOption型を使う
  • 例外(Exception)の代わりに、処理が成功か失敗したかにResult型を使う
  • 回復不能なエラーが発生した場合は、panic!でプログラムの実行を停止させる

参考