Rust入門

【Rust入門】Option 型と Result 型の基本について分かりやすく解説

【Rust入門】Option 型と Result 型の基本について分かりやすく解説
naoki-hn

Rust で値の有無や処理の結果を安全に扱うための Option 型と Result 型について、基本的な使い方を初心者にも分かりやすく解説します。

Option 型と Result 型とは

プログラミングをしている際には、「値が存在しないかもしれない」「結果として処理が失敗するかもしれない」という状況を適切に処理しなければならない場合があります。

Rust には、これらの状況を型として表現する仕組みがあります。それが Option 型と Result 型であり、Rust プログラミングにおいては非常に重要な型です。

この記事では、Option 型と Result 型の 2 つの型について紹介します。

なぜ Option 型・Result 型が必要なのか

Python や Java などの多くの言語では、値が存在しないことを Nonenull などで表現します。しかし、変数がどのような型であっても None が代入できると「値が入るまでの間で一時的に None が設定されているのか」「値の取得に失敗して None になっているのか」などが不明確で曖昧なコードとなり、実行時にエラーが発生しやすくなります。

なお、多くの言語には、上記のようなエラー処理に例外(Exception)機構を採用しています。呼び出し元の関数では、呼び出し先で発生しうる例外のバリエーションの考慮をする必要がありますが、すべてのパターンの把握は非常に大変で「補足することを忘れる」「複数の例外を親クラスなどでまとめて補足して曖昧にエラー処理を書く」「とりあえず上位に例外を投げておく」というような対処をしてしまいがちです。

Rust では、これらの問題をコンパイル時に防ぐために「値があるかもしれないし、ないかもしれない」ことを Optionで、「処理が成功するかもしれないし、失敗するかもしれない」ことを Resultで表現します。

どちらの型も、列挙型(enum)として言語仕様に組み込まれており、これらの型を受け取った呼び出し側は、中身を扱う際に「値の有/無」「処理の成功/失敗」を明確に意識したコードを書くことをコンパイラに強制されます。

エラー処理を後回しにできないことは、一見面倒ではありますが、結果として、「起こり得る失敗が型として明示され、対処漏れがコンパイル時に発見できる非常に見通しの良いプログラム」になります。これが、Rust において Option 型や Result 型が非常に重要で必要な理由です。

Option 型とは

Option<T> 型は、ある型 T の値が「存在する」か「存在しないか」を表すための型です。Rust では、以下のように定義されています。

enum Option<T> {
    None,
    Some(T),
}
  • 値が存在する場合は Some(T 型の値)
  • 値が存在しない場合は None

例えば、データから特定キーで検索するメソッドや条件に合う要素を探すメソッドなど「見つかるとは限らない」処理の戻り値として広く使われます。

実務での例では、例えば設備のセンサーから値を取得するような場面でも、通信異常やセンサーの一時停止によって値が取得できない(=欠損する)ことがあります。こうした「値が存在するかもしれないし、存在しないかもしれない」という状態をそのまま型として表現できることが、Option 型の強みです。

Option の詳細は、Option の公式ドキュメントも参考にしてください。

Result 型とは

Result<T, E> 型は、処理が「成功したか」「失敗したか」を表すための型です。Rust では、以下のように定義されています。

enum Result<T, E> {
    Ok(T),
    Err(E),
}
  • 処理が成功した場合は Ok(成功時の T 型の値)
  • 処理が失敗した場合は Err(失敗時の E 型の値)

Option が「値が存在するか」「値が存在しないか」の 2 択であったのに対して、Result は、「成功したら T 型の結果の値を取得」し、失敗した場合は「E 型のエラー情報を持つ」ように拡張した型であると理解できます。

Rust の関数において、失敗する可能性がある処理の戻り値は Result 型で設計することが一般的で、TE の型についてはそれぞれの関数の設計によります。Result 型について理解しておくことは、Rust の関数を理解するためには非常に重要です。

Result の詳細は、Result の公式ドキュメントも参照してください。

Option 型・Result 型の使い方

Option 型の生成方法と主要メソッド

生成方法(Some / None

Option 型の値は、値がある場合は Some、値がない場合は None として生成します。以下のような簡単な除算を行う関数で考えてみましょう。

/// 除算を行う関数
/// 分母が 0 の場合は `None` を返し、それ以外の場合は `Some(結果)` を返す。
fn divide(numerator: f64, denominator: f64) -> Option<f64> {
    if denominator == 0.0 {
        None
    } else {
        Some(numerator / denominator)
    }
}

fn main() {
    let result1 = divide(10.0, 2.0);
    println!("result1: {result1:?}");

    let result2 = divide(10.0, 0.0);
    println!("result2: {result2:?}");
}
【実行結果】
result1: Some(5.0)
result2: None

0 で割った場合は、演算結果自体が定義できないため None を返却するように関数を定義しています。ここでは、「なぜ割れないのか」という理由までは持たせずに、単に「値が得られたどうか」だけを表現しています。後ほど Result 版での divide 関数も紹介するので、違いを確認していただければと思います。

値を取り出す(unwrap / expect / unwrap_or / unwrap_or_else

Option の中身を取り出すには、いくつかの方法があります。

  • unwrap : 値が存在する前提で取り出す。(None の場合はパニック(異常終了))
  • expect : パニック時に表示するメッセージを指定する。
  • unwrap_or : None の場合にデフォルト値を指定する。
  • unwrap_or_else : None の場合の処理をクロージャで指定する。

以下の例で見てみましょう。(※以降の例では、divide 関数は同じなので省略します。)

fn main() {
    // unwrap : 値が存在する前提で取り出す(None の場合はパニック)
    // divide(10,0, 0.0) に変更するとパニックします
    let result1 = divide(10.0, 2.0).unwrap();
    println!("unwrap : {result1}");

    // expect : パニック時に表示するメッセージを指定する
    // divide(10,0, 0.0) に変更するとパニックします
    let result2 = divide(10.0, 2.0).expect("0 では割れません。");
    println!("expect : {result2}");

    // unwrap_or : None の場合にデフォルト値を指定する
    let result3 = divide(10.0, 0.0).unwrap_or(0.0);
    println!("unwrap_or : {result3}");

    // unwrap_or_else : None の場合の処理をクロージャで指定する
    let result4 = divide(10.0, 0.0).unwrap_or_else(|| {
        println!("0 では割れません。");
        0.0
    });
    println!("unwrap_or_else : {result4}");
}
【実行結果】
unwrap : 5
expect : 5
unwrap_or : 0
0 では割れません。
unwrap_or_else : 0

unwrapexpect は、None だった場合にプログラムがパニック(異常終了)します。expect は、unwrap とは異なり、パニック時に表示するメッセージを指定できるため、デバッグ時になぜ失敗したのかが判別しやすくなります。

一方、unwrap_orunwrap_or_else はパニックとはならず、None の代わりの値を返します。unwrap_or は固定値を、unwrap_or_else はクロージャで動的に生成した値を使いたい場合に使用できます。

注意点

unwrap / expect の利用場面

unwrapexpect はプロトタイプ作成中や事前条件から値を持つ (Some になる) ようなケースに限定して使うようにしましょう。None がよく発生するようなケースで使用してしまうとプログラムが不安定になります。基本は、後述するパターンマッチングや ? 演算子による上位への移譲で明確に処理することが重要です。

値を変換する(map

map メソッドを使用すると、値が存在する場合のみ中身を変換することができます。

fn main() {
    // 値が存在する場合のみ、中身を変換する
    let doubled = divide(10.0, 2.0).map(|x| x * 2.0);
    println!("値が存在する場合に変換される : {doubled:?}");

    // 値が存在しない場合は、中身は変換されない
    let doubled_none = divide(10.0, 0.0).map(|x| x * 2.0);
    println!("値が存在しない場合は変換されない : {doubled_none:?}");
}
【実行結果】
値が存在する場合に変換される : Some(10.0)
値が存在しない場合は変換されない : None

上記例では、Option の値が存在する場合のみ、値を 2 倍にしています。変換はクロージャを使って指定できます。

map の便利な点は、Some であるのか、None であるのかを自分で判定しなくても、値がある場合だけ安全に変換処理を適用できるところです。対象が None の場合は、例のように変換は実行されず、そのまま None が返却されます。

値の有無に応じて処理を連鎖する(and_then

and_then メソッドを使用すると、Option を返却する別の処理をさらに連鎖できます。

fn main() {
    // divide 処理を連鎖させる
    let result = divide(10.0, 2.0).and_then(|x| divide(x, 2.0));
    println!("連鎖結果 : {result:?}");
}
【実行結果】
連鎖結果 : Some(2.5)

map との違いは、渡す関数(またはクロージャ)の戻り値です。map は、普通の値を返す関数を渡すのに対し、and_then は「Option を返す関数」を渡します。

mapOption を返す関数を渡すと返却値は Option<Option<T>> のようにネストしてしまいますが、and_then ではネストを避け、Option<T>のまま処理をつなげることができます。

注意点

Optionand_thenOption を返す関数のみ連鎖できる

and_then に渡す関数は、Option を返す関数である必要があります。普通の値や Result を返す関数を渡すことはできない点に注意してください。例えば、Result を返す関数に連鎖させたい場合は、後述する ok_or で先に型変換をする必要があります。

Result 型への変換(ok_or

ok_or メソッドを使用すると、OptionResult に変換できます。None だった場合に、指定した任意のエラー情報を持たせることができます。

fn main() {
    // None の場合にエラー情報を持たせて Result に変換する
    let result: Result<f64, &str> = divide(10.0, 0.0).ok_or("0 で割ることはできません");
    println!("{result:?}");

    // Some の場合は Ok に変換される
    let result: Result<f64, &str> = divide(10.0, 2.0).ok_or("0 で割ることはできません");
    println!("{result:?}");
}
【実行結果】
Err("0 で割ることはできません")
Ok(5.0)

Result 型の生成方法と主要メソッド

ここまでに見てきたように、Option では、「値が得られたかどうか」だけを表現し、失敗の理由を判別することができませんでした。以降では、Result を使用するとどのように変わってくるのかを見ていきましょう。

生成方法(Ok / Err

Result 型の値は、処理が成功した場合は Ok、失敗した場合は Err として生成します。Option の例と同じように除算を行う例で見てみましょう。

/// 除算を行う関数
/// 分母が 0 の場合はエラー( `Err` )を返し、そうでなければ計算結果( `Ok` )を返す。
fn divide(numerator: f64, denominator: f64) -> Result<f64, String> {
    if denominator == 0.0 {
        Err(String::from("0 で割ることはできません"))
    } else {
        Ok(numerator / denominator)
    }
}

fn main() {
    let result1 = divide(10.0, 2.0);
    println!("result1: {result1:?}");

    let result2 = divide(10.0, 0.0);
    println!("result2: {result2:?}");
}
【実行結果】
result1: Ok(5.0)
result2: Err("0 で割ることはできません")

divide 関数のように、失敗する可能性がある処理の戻り値は、Result<成功時の型, エラーの型> として定義するのが基本パターンです。Option 版の divide 関数と比べると、Result 版では「0 で割ることはできません」という失敗理由を Err の中に持たせることができていることが分かります。

値を取り出す(unwrap / expect / unwrap_or / unwrap_or_else

Result の中身を取り出す方法は、Option と同様で、unwrapexpectunwrap_orunwrap_or_else を使用できます。
(※以降の例では、divide 関数は同じなので省略します。)

fn main() {
    // unwrap : 値が存在する前提で取り出す(Err の場合はパニック)
    // divide(10.0, 0.0) に変更するとパニックします
    let result1 = divide(10.0, 2.0).unwrap();
    println!("unwrap : {result1}");

    // expect : パニック時に表示するメッセージを指定する
    // divide(10.0, 0.0) に変更するとパニックします
    let result2 = divide(10.0, 2.0).expect("0 では割れません。");
    println!("expect : {result2}");

    // unwrap_or : Err の場合にデフォルト値を指定する
    let result3 = divide(10.0, 0.0).unwrap_or(0.0);
    println!("unwrap_or : {result3}");

    // unwrap_or_else : Err の場合の処理をクロージャで指定する
    let result4 = divide(10.0, 0.0).unwrap_or_else(|err| {
        println!("{err}");
        0.0
    });
    println!("unwrap_or_else : {result4}");
}
【実行結果】
unwrap : 5
expect : 5
unwrap_or : 0
0 で割ることはできません
unwrap_or_else : 0

Option と同様で、unwrapexpectErr の場合にパニックになります。Resultunwrap_or_else では、クロージャの引数として、エラーの中身(String など)を受け取れる点が Option との違いです。

値を変換する(map

map メソッドでは、Ok の場合のみ中身を変換することができます。

fn main() {
    // 値が成功(Ok)の場合のみ、中身を変換する
    let doubled = divide(10.0, 2.0).map(|x| x * 2.0);
    println!("値が成功の場合のみ変換される : {doubled:?}");

    // 値が失敗(Err)の場合は、中身は変換されない
    let doubled_error = divide(10.0, 0.0).map(|x| x * 2.0);
    println!("値が失敗の場合は変換されない : {doubled_error:?}");
}
【実行結果】
値が成功の場合のみ変換される : Ok(10.0)
値が失敗の場合は変換されない : Err("0 で割ることはできません")

処理を連鎖する(and_then

and_then メソッドを使用すると、Result を返却する別の処理をさらに連鎖できます。

fn main() {
    // divide 処理を連鎖させる
    let result = divide(10.0, 2.0).and_then(|x| divide(x, 2.0));
    println!("連鎖結果 : {result:?}");
}
【実行結果】
連鎖結果 : Ok(2.5)
注意点

Resultand_then は同じエラー型の Result を返す関数のみ連鎖できる

and_then に渡す関数は、必ず元の Result と同じエラー型を持つ Result を返す必要があります。例えば、Result<T, String> に対しては、渡す関数も Result<U, String> を返す必要があります。Result<U, io::Error> のような別の型を返す関数をそのまま渡すことはできない点に注意してください。

Option 型への変換(ok

ok メソッドを使用すると、ResultOption に変換できます。Optionok_or とは逆方向の変換で、エラー情報を捨てて成功時の値を残します。

fn main() {
    // Ok の場合に、エラー情報を捨てて Option の Some に変換する
    let ok_some = divide(10.0, 2.0).ok();
    println!("成功時 : {ok_some:?}");

    // Err の場合に、エラー情報を捨てて Option の None に変換する
    let err_none = divide(10.0, 0.0).ok();
    println!("失敗時 : {err_none:?}");
}
【実行結果】
成功時 : Some(5.0)
失敗時 : None

ok_or が「Option に理由を付け加えて Result に変換する方法」であったのに対し、ok は「Result からエラーを取り除いて Option に変換する方法」です。OptionResult は、ok_orok によって相互変換できる関係にあります。

OptionResult の値の取り出しとエラー処理

以降では、OptionResult を具体的に処理する代表的な方法例を紹介します。

パターンマッチによる分岐処理

OptionResult の中身を安全に取り出し、ケースごとに処理を分けるには、match によるパターンマッチングが有効です。

以下は、Result の例で紹介しますが、Option でも考え方は同様です。
(※ divide 関数は 上記で紹介した Result 版のものと同じため省略します。)

fn main() {
    // Result のパターンマッチ (成功の Ok の場合)
    let result_ok = divide(10.0, 2.0);
    match result_ok {
        Ok(value) => println!("成功 : {value}"),
        Err(err) => println!("失敗 : {err}"),
    }
    // Result のパターンマッチ (失敗の Err の場合)
    let result_err = divide(10.0, 0.0);
    match result_err {
        Ok(value) => println!("成功 : {value}"),
        Err(err) => println!("失敗 : {err}"),
    }
}
【実行結果】
成功 : 5
失敗 : 0 で割ることはできません

match を使うことで unwrap のようにパニックのリスクを負うことなく、 Some / NoneOk / Err の各ケースに応じた処理を記載することができます。

Rust の match では、すべてのパターンの処理を記載しないとコンパイルをすることができません。この強制により、Rust では安全なプログラムを実現できます。

パターンマッチングについては以下を参考にしてください。

パターンマッチングの基本について分かりやすく解説

? 演算子による呼び出し元への伝播

OptionResult は、? 演算子を用いることで値がない場合やエラーの場合についてその場で処理するのではなく、上位の呼び出し元に返して処理を移譲することができます。

以下は、Result の例で紹介しますが、Option でも考え方は同様です。
(※ divide 関数は 上記で紹介した Result 版のものと同じため省略します。)

fn calculate(a: f64, b: f64) -> Result<f64, String> {
    // エラーの場合は `?` 演算子によって呼び出し元に返される
    let result = divide(a, b)?;

    Ok(result)
}
fn main() {
    let result1 = calculate(10.0, 2.0);
    match result1 {
        Ok(value) => println!("result1: {value}"),
        Err(err) => println!("error: {err}"),
    }

    let result2 = calculate(10.0, 0.0);
    match result2 {
        Ok(value) => println!("result2: {value}"),
        Err(err) => println!("error: {err}"),
    }
}
【実行結果】
result1: 5
error: 0 で割ることはできません

? を式の末尾に付けると、SomeOk の場合は中身を取り出し処理を続け、NoneErr の場合は、その場で関数から NoneErr を早期リターンします。

上記例では、calculate 関数で divide を呼び出した際に Err だった場合は、mainErr をそのまま伝播します。main 関数では、伝わってきた情報を match で処理しています。

エラー処理の基本については以下を参考にしてください。

エラー処理の基本を分かりやすく解説

まとめ

Rust で値の有無や処理の結果を安全に扱うための Option 型と Result 型について、基本的な使い方を解説しました。

この記事では、OptionResult のそれぞれの生成方法や各種主要なメソッドの使い方に加えて、match による処理方法や ? 演算子によるエラー伝播についても紹介しました。

OptionResult は「値がないかもしれない」「処理が失敗するかもしれない」という可能性を型で表現して対処をコンパイラに強制させる仕組みで、Rust ではとても重要な型です。これにより、他言語でよく発生する nullNone の参照によるクラッシュや例外の補足し忘れといった不具合を言語レベルで防止することができます。

これらの型を適切に使うことで見通しがよく安全なプログラムを記述できることが Rust の大きな魅力の一つです。ぜひ基本を理解して適切に使いこなせるようになってください。

ソースコード

上記で紹介しているソースコードについては GitHub にて公開しています。参考にしていただければと思います。

あわせて読みたい
Rust プログラミング入門
Rust プログラミング入門

ABOUT ME
ホッシー
ホッシー
システムエンジニア
はじめまして。当サイトをご覧いただきありがとうございます。
私は製造業のメーカーで、DX推進や業務システムの設計・開発・導入を担当しているシステムエンジニアです。これまでに転職も経験しており、以前は電機メーカーでシステム開発に携わっていました。

これまでの業務を通じてさまざまなプログラミング言語や技術に触れてきましたが、その中でもRustの設計思想に惹かれ、この言語についてもっと深く学びたい、そしてその魅力を発信していきたいと思い、このサイトを立ち上げました。

自身の学びを整理しつつ、同じようにRustに興味を持つ方のお役に立てるような情報を発信していければと思っています。どうぞよろしくお願いいたします。

※キャラクターデザイン:ゼイルン様
記事URLをコピーしました