Result で回復可能なエラーを処理する

RustBeginner
オンラインで実践に進む

はじめに

プログラミングのバグによって発生する失敗もありますが、実行時に予測される失敗もあります。たとえば、ユーザーが形式の正しくないテキストを入力したり、許可された範囲外の値を入力したりする場合です。Rust では、このような回復可能な結果を Result<T, E> で表します。Result<T, E> は Ok(T) または Err(E) のどちらかです。

この実験では、パニックを発生させずに正しいテキストと正しくないテキストを解析し、内容のある検証エラーを返し、? 演算子を使ってエラーを呼び出し元へ伝播させます。用意されている Cargo プロジェクトでは、各編集作業をエラー処理に集中できます。

成功した Result をパターンマッチする

このステップでは、テキストを整数に解析し、成功した結果をパターンマッチします。

プロジェクトは /home/labex/project/result-workshop にあり、src/main.rs には4つの作業用プレースホルダーがあります。プロジェクトへ移動し、ソースファイルを開きます。

cd /home/labex/project/result-workshop
nano src/main.rs

Step 1 のコメントを次の内容に置き換えます。

    let valid = "42".parse::<i32>();
    match valid {
        Ok(value) => println!("Parsed value: {value}"),
        Err(error) => println!("Parse error: {error}"),
    }

parse は、さまざまな出力型を生成できる再利用可能なメソッドです。::<i32> の部分は、この呼び出しで使用する出力型を指定し、Rust に文字列を符号付き32ビット整数として解釈させます。この山括弧内の型引数は、どちらも :: を使いますが、Message::Text のようなパスとは異なります。解析に失敗する可能性があるため、戻り値の型は Result です。Ok(value) には解析された整数が入り、Err(error) には解析に失敗した理由の情報が入ります。match では、両方のバリアントを処理する必要があります。

Ctrl+O で保存し、Enter を押してから、Ctrl+X で終了します。確認して実行します。

cargo check
cargo run --quiet
Parsed value: 42

"42" は正しい整数形式のテキストなので、Ok アームだけが実行されます。

正しくない入力から回復する

このステップでは、形式の正しくないテキストを、プログラムをクラッシュさせるのではなくデータとして処理します。

パニックは、プログラムが設計上想定していない状態に到達したことを示し、通常は実行を停止させます。一方、外部から受け取る形式の正しくない入力は想定されるものです。そのため、プログラムが検査して報告できる Err のまま扱う必要があります。ソースファイルを開きます。

nano src/main.rs

Step 2 のコメントを次の内容に置き換えます。

    let invalid = "many".parse::<i32>();
    match invalid {
        Ok(value) => println!("Unexpected value: {value}"),
        Err(error) => println!("Handled invalid input: {error}"),
    }

この match も、考えられる両方の結果を処理しています。ただし、用意されたテキストによって Err アームが選ばれます。保存して終了し、実行します。

cargo run --quiet

2行目に次の内容が表示されます。

Handled invalid input: invalid digit found in string

プログラムは正常に実行を続け、パーサーのエラーを有用なメッセージに変換しました。外部データに対して unwrap() を使うのは避けてください。同じ Err が発生した場合、unwrap() は回復の機会を与えずにパニックを発生させます。

疑問符演算子で解析と検証を行う

このステップでは、パーセント値または有用なエラーメッセージを返せる関数を作成します。

型 Result<u32, String> は、成功時には符号なし整数を、失敗時には所有権を持つエラーテキストを返すことを表します。? 演算子は Ok から値を取り出します。Err を検出した場合は、そのエラーを現在の関数から直ちに返します。

ソースファイルを開きます。

nano src/main.rs

main の上に次の関数を追加します。

fn parse_percentage(text: &str) -> Result<u32, String> {
    let value = text
        .parse::<u32>()
        .map_err(|_| format!("not a whole number: {text}"))?;

    if value <= 100 {
        Ok(value)
    } else {
        Err(format!("outside 0..=100: {value}"))
    }
}

この関数は、短い処理の流れとして読むことができます。まず、parse::<u32>() がテキストを整数に変換しようとします。次に、map_err(...) が解析に失敗した場合だけ、元の入力を含む、より明確な String にエラーを変換します。

続いて ? は、解析に成功した数値を value に取り出すか、新しいエラーを parse_percentage から直ちに返します。解析に成功した場合は、if で数値を検証し、Ok(value) または範囲外を示す Err を返します。

クロージャの引数 _ は、元のパーサーエラーの値を意図的に無視することを示します。新しいメッセージで置き換えるためです。ここでの _ は無視する引数です。これに対して、先ほどの _ => はワイルドカードの match アームであり、let _ = は結果全体を破棄する記述です。

main 内の Step 3 のコメントを次の内容に置き換えます。

    println!("85 => {:?}", parse_percentage("85"));
    println!("150 => {:?}", parse_percentage("150"));
    println!("many => {:?}", parse_percentage("many"));

この学習実験では、:? フォーマッターを使って Ok または Err のバリアントを表示します。保存して終了し、確認して実行します。

cargo check
cargo run --quiet

追加した行は次のようになります。

85 => Ok(85)
150 => Err("outside 0..=100: 150")
many => Err("not a whole number: many")

これで関数は、正しい値、解析はできたものの範囲外の値、形式の正しくないテキストを区別できるようになりました。

別の関数を通してエラーを伝播させる

このステップでは、2つ目の関数境界で ? を使い、最終的な結果を main で処理します。

ヘルパー関数が、エラーをアプリケーションでどのように表示または回復するかを常に判断できるとは限りません。その場合、Err を上位へ伝播させ、呼び出し元に判断を委ねることができます。ソースファイルを開きます。

nano src/main.rs

parse_percentage と main の間に、次の関数を追加します。

fn acceptance_message(text: &str) -> Result<String, String> {
    let value = parse_percentage(text)?;
    Ok(format!("accepted {value}%"))
}

解析または検証に失敗すると、? は既存の String エラーを直ちに返します。成功した場合は、メッセージを Ok で包んで返します。

main 内の Step 4 のコメントを次の内容に置き換えます。

    for text in ["73", "bad"] {
        match acceptance_message(text) {
            Ok(message) => println!("{text} => {message}"),
            Err(message) => println!("{text} => error: {message}"),
        }
    }

このループには、成功する入力と失敗する入力が1つずつ含まれています。各結果をユーザー向けの出力に変換する境界が main です。保存して終了し、確認して実行します。

cargo check
cargo run --quiet

最後の行は次のようになります。

73 => accepted 73%
bad => error: not a whole number: bad

parse_percentage で作成された同じエラーコンテキストが、acceptance_message を通じて伝播されました。

まとめ

成功または失敗した Result の値をパターンマッチし、形式の正しくない入力を回復可能な状態として扱い、map_err でコンテキストを含む String エラーを作成しました。また、unwrap でパニックを発生させる代わりに、? を使って関数境界を越えてエラーを伝播させました。