[Rustのはじめ方] Part14: Option型とResult型の扱い

Rust

値が存在しない可能性や、処理が失敗する可能性を安全に扱う方法を学びましょう。

Rustは、他の多くのプログラミング言語にある nullnil のような「値がない」ことを示す特別な値を持っていません。その代わりに、値が存在しない可能性を表現するために Option<T> 型を、処理が成功したか失敗したかを示すために Result<T, E> 型を使用します。これらの型は列挙型(enum)として定義されており、Rustの強力な型システムとパターンマッチングによって、これらの状況を安全かつ明示的に扱うことができます。これにより、コンパイル時に潜在的な問題を検出し、実行時エラー(特にnullポインタ関連のエラー)を防ぐのに役立ちます。👍

Option<T>型:値が存在しないかもしれない場合

Option<T> は、値が「存在する」(Some(T))か、「存在しない」(None)かのどちらかであることを示す列挙型です。TSome バリアントが保持する値の型(ジェネリック型)を表します。

Option<T> の定義

標準ライブラリで以下のように定義されています。

pub enum Option<T> {
    None,    // 値が存在しない
    Some(T), // 値 T が存在する
}

値がないかもしれない状況、例えばハッシュマップからのキー検索や配列の範囲外アクセスなどでよく使われます。None は値がないことを示しますが、それ自体がエラーを意味するわけではありません。

Option<T> を返す関数の例

例えば、数値のリストの中から偶数を見つける関数を考えてみましょう。偶数が見つかればその値を、見つからなければ「見つからなかった」ことを示したい場合に Option<i32> を返すことができます。

fn find_first_even(numbers: &[i32]) -> Option<i32> {
    for &num in numbers {
        if num % 2 == 0 {
            return Some(num); // 最初に見つかった偶数を Some で包んで返す
        }
    }
    None // 見つからなければ None を返す
}

fn main() {
    let nums1 = [1, 3, 5, 6, 7, 8];
    let first_even1 = find_first_even(&nums1);
    println!("最初の偶数 (nums1): {:?}", first_even1); // Some(6)

    let nums2 = [1, 3, 5, 7, 9];
    let first_even2 = find_first_even(&nums2);
    println!("最初の偶数 (nums2): {:?}", first_even2); // None
}

Option<T> の値の扱い方

Option<T> 型の値を受け取った場合、それが Some なのか None なのかを判別して、中の値を取り出す必要があります。Rustでは、Option の中の値に直接アクセスすることはできず、必ず以下のいずれかの方法で明示的に処理する必要があります。

1. match 式

最も網羅的で基本的な方法です。Some(T)None の両方のケースに対する処理を記述します。

fn process_option(opt: Option<&str>) {
    match opt {
        Some(value) => println!("値が見つかりました: {}", value),
        None => println!("値が見つかりませんでした。"),
    }
}

fn main() {
    let name: Option<&str> = Some("Alice");
    process_option(name); // 値が見つかりました: Alice

    let age: Option<&str> = None;
    process_option(age); // 値が見つかりませんでした。
}

2. if let 式

Some の場合だけ特定の処理を行いたい場合に便利です。match よりも簡潔に書けます。

fn main() {
    let maybe_value: Option<i32> = Some(5);

    if let Some(value) = maybe_value {
        println!("値は {} です。", value); // こちらが実行される
    } else {
        println!("値はありません。");
    }

    let no_value: Option<i32> = None;
    if let Some(value) = no_value {
        // このブロックは実行されない
        println!("値は {} です。", value);
    } else {
        println!("値はありません。"); // こちらが実行される
    }
}

3. unwrap() メソッド 注意

Some(T) から値 T を直接取り出します。しかし、OptionNone の場合に呼び出すとプログラムが panic してクラッシュします。プロトタイピングやテストなど、絶対に Some であることが保証されている、あるいは panic しても問題ない場合を除き、本番コードでの使用は推奨されません。

fn main() {
    let value = Some(100).unwrap(); // value は 100 になる
    println!("value: {}", value);

    // let value_none = None::<i32>.unwrap(); // ここで panic する! 💥
    // println!("value_none: {}", value_none);
}

4. expect(&str) メソッド 注意

unwrap() と似ていますが、None で panic する際に、引数で指定したカスタムエラーメッセージを表示します。なぜ値が存在するはずだと期待していたのかを示すメッセージを記述できるため、unwrap() よりはデバッグに役立ちますが、panic するリスクは同じです。

fn main() {
    let value = Some(200).expect("値があるはずでした"); // value は 200
    println!("value: {}", value);

    // let value_none = None::<i32>.expect("None ではないと期待していましたが、None でした!"); // ここで panic し、指定したメッセージが表示される 💥
    // println!("value_none: {}", value_none);
}

5. unwrap_or(default) メソッド

Some(T) なら中の値 T を返し、None なら引数で指定したデフォルト値を返します。panic しないので安全です。

fn main() {
    let value = Some(300).unwrap_or(0); // value は 300
    println!("value: {}", value);

    let default_value = None::<i32>.unwrap_or(0); // default_value は 0
    println!("default_value: {}", default_value);
}

6. unwrap_or_else(closure) メソッド

Some(T) なら中の値 T を返します。None なら引数で指定したクロージャ(関数)を実行し、そのクロージャが返した値を返します。デフォルト値の計算にコストがかかる場合や、None の場合にのみ特定の処理を行いたい場合に便利です。クロージャは None の場合にのみ実行されます。

fn main() {
    let value = Some(400).unwrap_or_else(|| 0); // value は 400
    println!("value: {}", value);

    let calculated_default = None::<i32>.unwrap_or_else(|| {
        println!("デフォルト値を計算中..."); // None の場合のみ実行される
        -1 // 計算結果として -1 を返す
    }); // calculated_default は -1
    println!("calculated_default: {}", calculated_default);
}

その他の便利なメソッド

Option には他にも多くの便利なメソッドがあります。

  • map(closure): Some(T) なら中の値に関数を適用して Some(U) を返し、None なら None を返す。
  • and_then(closure): Some(T) なら中の値に関数を適用し、その関数が返す Option<U> を返す。None なら None を返す (flatMap とも呼ばれる)。
  • filter(predicate): Some(T) の値が条件を満たすなら Some(T) を返し、そうでなければ None を返す。
  • ok_or(err) / ok_or_else(err_fn): Option<T>Result<T, E> に変換する。Some(T)Ok(T) に、None は指定されたエラー Err(E) になる。
💡 ポイント: unwrap()expect() は、プログラムのロジック上絶対に None にならないことが保証されている場合や、簡単なサンプル、テストコードなど、panic しても問題ない限定的な状況での使用に留めましょう。通常のアプリケーションコードでは、match, if let, unwrap_or, unwrap_or_else, map, and_then などの安全なメソッドを使うのが一般的です。

Result<T, E>型:成功または失敗を表す

Result<T, E> は、操作が成功した(Ok(T))か、失敗した(Err(E))かを示す列挙型です。T は成功した場合に保持される値の型、E は失敗した場合に保持されるエラーの型を表します。Rustにおける主要なエラーハンドリング機構です。

Result<T, E> の定義

標準ライブラリで以下のように定義されています。

pub enum Result<T, E> {
    Ok(T),  // 成功。値 T を保持する
    Err(E), // 失敗。エラー E を保持する
}

ファイル操作、ネットワーク通信、データパースなど、失敗する可能性のある多くの標準ライブラリ関数が Result を返します。

Result<T, E> を返す関数の例

文字列を数値にパースする操作は失敗する可能性があります(例: “abc” を数値にはできない)。標準ライブラリの parse メソッドは Result<T, ParseIntError> を返します。

// use std::num::ParseIntError; // main 内で使うので use は不要な場合も

fn parse_number(s: &str) -> Result<i32, std::num::ParseIntError> {
    // 文字列スライスの parse メソッドは Result を返す
    s.parse::<i32>()
}

fn main() {
    let number_str = "123";
    let not_number_str = "abc";

    let result1 = parse_number(number_str);
    println!("Result 1: {:?}", result1); // Ok(123)

    let result2 = parse_number(not_number_str);
    println!("Result 2: {:?}", result2); // Err(ParseIntError { kind: InvalidDigit })
}

Result<T, E> の値の扱い方

Result<T, E> 型の値も、Option と同様に、Ok なのか Err なのかを判別して処理する必要があります。

1. match 式

Ok(T)Err(E) の両方のケースを明示的に扱います。

fn print_result(res: Result<i32, std::num::ParseIntError>) {
    match res {
        Ok(value) => println!("パース成功: {}", value),
        Err(e) => println!("パース失敗: {:?}", e), // エラーの詳細も表示できる
    }
}

fn main() {
    print_result("456".parse()); // パース成功: 456
    print_result("xyz".parse()); // パース失敗: ParseIntError { kind: InvalidDigit }
}

2. if let 式

Ok の場合や Err の場合だけ特定の処理を行いたい時に使えます。

fn main() {
    let result_ok: Result<i32, String> = Ok(10);
    if let Ok(value) = result_ok {
        println!("成功しました: {}", value); // 実行される
    }

    let result_err: Result<i32, String> = Err("何か問題が発生しました".to_string());
    if let Err(e) = result_err {
        println!("失敗しました: {}", e); // 実行される
    }
}

3. unwrap() メソッド 注意

Ok(T) から値 T を取り出します。Err(E) の場合に呼び出すとプログラムが panic します。Optionunwrap() 同様、安易な使用は避け、他の安全な方法を使うべきです。

fn main() {
    // let number = "123".parse::<i32>().unwrap(); // number は 123
    // println!("number: {}", number);

    // let failed = "abc".parse::<i32>().unwrap(); // ここで panic する! 💥
    // println!("failed: {}", failed);
}

4. expect(&str) メソッド 注意

unwrap() と同様に Err(E) で panic しますが、その際に指定したエラーメッセージを表示します。

fn main() {
    // let number = "123".parse::<i32>().expect("数値へのパースに失敗しました"); // number は 123
    // println!("number: {}", number);

    // let failed = "abc".parse::<i32>().expect("数値へのパースに失敗しました"); // ここで panic し、指定したメッセージが表示される 💥
    // println!("failed: {}", failed);
}

5. unwrap_or(default), unwrap_or_else(closure) メソッド

Option と同様に、Err の場合にデフォルト値やクロージャの結果を返します。unwrap_or は成功時の型 T と同じ型のデフォルト値を、unwrap_or_else はエラー値 E を引数に取るクロージャを受け取ります。

fn main() {
    let ok_result: Result<i32, &str> = Ok(50);
    let err_result: Result<i32, &str> = Err("データ取得エラー");

    let value1 = ok_result.unwrap_or(0); // value1 は 50
    println!("Value1: {}", value1);

    let value2 = err_result.unwrap_or(0); // value2 は 0 (デフォルト値)
    println!("Value2: {}", value2);

    let value3 = err_result.unwrap_or_else(|e| {
        println!("エラーが発生 ({}) したのでデフォルト値を計算します。", e);
        -1 // デフォルト値として -1 を返す
    });
    println!("Value3: {}", value3); // value3 は -1
}

6. ok(), err() メソッド

Result<T, E>Option<T>Option<E> に変換します。

  • ok(): Ok(T) なら Some(T) を、Err(E) なら None を返します。
  • err(): Ok(T) なら None を、Err(E) なら Some(E) を返します。
fn main() {
    let ok_val: Result<i32, &str> = Ok(10);
    let err_val: Result<i32, &str> = Err("error");

    assert_eq!(ok_val.ok(), Some(10));
    assert_eq!(ok_val.err(), None);

    assert_eq!(err_val.ok(), None);
    assert_eq!(err_val.err(), Some("error"));
}

7. ? 演算子 (Question Mark Operator) ✨

エラーハンドリングを劇的に簡潔にするための非常に重要な演算子です。Result を返す関数内で、別の Result を返す処理(関数呼び出しなど)の直後に ? を付けます。

  • もし処理結果が Ok(T) なら、? は中の値 T を取り出して、そのまま処理を続行させます。
  • もし処理結果が Err(E) なら、? はその場で現在の関数から Err(E) を返します。これにより、エラーが自動的に呼び出し元に伝播します。

注意: ? 演算子は、戻り値の型が Result<T, E>(または Option<T> や、Try トレイトを実装する他の型)である関数内でのみ使用できます。

例として、ファイルを開いてその内容を文字列として読み込む関数を見てみましょう。

use std::fs::File;
use std::io::{self, Read}; // io::Error のために io を use

// ? を使わない場合のエラー処理 (match のネスト)
fn read_username_from_file_verbose() -> Result<String, io::Error> {
    let f_result = File::open("username.txt");

    let mut f = match f_result {
        Ok(file) => file,
        Err(e) => return Err(e), // エラーなら即座に関数から Err を返す
    };

    let mut s = String::new();

    match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        Err(e) => return Err(e), // エラーなら即座に関数から Err を返す
    }
}

// ? を使った場合のエラー処理 (非常に簡潔!)
fn read_username_from_file_concise() -> Result<String, io::Error> {
    // File::open が Err を返したら、? がここで return Err(e) する
    let mut f = File::open("username.txt")?;
    let mut s = String::new();
    // read_to_string が Err を返したら、? がここで return Err(e) する
    f.read_to_string(&mut s)?;
    // すべて成功したら Ok(s) を返す
    Ok(s)
}

// さらにチェインして短く書くことも可能
fn read_username_from_file_shorter() -> Result<String, io::Error> {
    let mut s = String::new();
    // File::open(...)?.read_to_string(...) と ? を繋げられる
    File::open("username.txt")?.read_to_string(&mut s)?;
    Ok(s)
}

// 標準ライブラリにはさらに便利な関数 std::fs::read_to_string がある
fn read_username_from_file_shortest() -> Result<String, io::Error> {
    std::fs::read_to_string("username.txt")
}

fn main() {
    // 実行する際は username.txt ファイルの有無で結果が変わる
    match read_username_from_file_concise() {
        Ok(username) => println!("ユーザー名: {}", username),
        Err(e) => println!("エラー: {}", e),
    }
}
🔥 重要: ? 演算子は、エラーが発生する可能性のある処理を連続して呼び出す場合に、コードを非常にすっきりとさせます。エラーが発生した場合は即座に関数から離脱してエラーを呼び出し元に伝え、成功した場合は中の値を返して処理を続ける、という定型的なエラー伝播のロジックを簡潔に記述できます。

まとめ

  • Rust は null の代わりに、値の不在を Option<T> (Some(T) / None) で、回復可能なエラーを Result<T, E> (Ok(T) / Err(E)) で表現します。
  • これらの型は列挙型であり、matchif let を使って安全に中身を処理します。
  • unwrap()expect()NoneErr の場合に panic を引き起こすため、注意して使用する必要があります。
  • unwrap_or()unwrap_or_else() は、NoneErr の場合に代替となる値を提供するための安全な方法です。
  • Result を返す関数内では、? 演算子を使うことでエラー伝播のコードを大幅に簡略化できます。

OptionResult は、Rustの安全性と表現力を支える重要な要素です。これらを効果的に使いこなすことで、コンパイラによる厳密なチェックの恩恵を受け、堅牢で信頼性の高いプログラムを構築することができます。次のトピックでは、回復不能なエラーを示す panic! について詳しく見ていきましょう。🚀

コメント

タイトルとURLをコピーしました