Rust の Option 型の基本 ─ 値の有無を表現する型

Option 型とは?

多くのオブジェクト指向言語には、オブジェクトが存在しないことを示す null という値が用意されていますが、Rust には null は存在しません。 Rust の設計者は、null という概念が不具合の温床となっていると判断しました。 その代わりに、Rust には std::option::Option という組み込みの列挙型 (enum) が用意されており、ある値が存在しているか を表現できるようになっています。 そして、この設計は null を使った表現よりも柔軟で、かつ安全です。

Option 型の定義はとてもシンプルで、次のような感じの列挙型 (enum) として定義されています。

Option 型の定義
pub enum Option<T> {
    Some(T),  // T 型の何らかの値
    None,     // 値が存在しない
}

Some バリアントが「(任意の型 T の)値が存在する」ことを示し、None バリアントが「値が存在しない」ことを示します。 つまり、SomeNone で値の有無を表現しつつ、値が存在する場合はその値を Some バリアントから取り出せるようになっています。

例えば、値が存在しないかもしれない String 型(他の言語では Nullable な String 型)は、Option<String> 型として表現することができ、その Some 値と None 値を次のように生成できます。

let some_val: Option<String> = Some(String::from("Hello"));
let none_val: Option<String> = None;  // 他の言語では null や nil に相当

こんな感じで別名を付けると理解しやすいでしょうか。

type NullableString = Option<String>;
let some_val: NullableString = Some(String::from("Hello"));
let none_val: NullableString = None;

Option は単なる列挙型なので、本来はバリアントを参照するときに Option::SomeOption::None と記述しなければならないはずですが、デフォルトで SomeNone と短く記述できるようになっています。 Option 型は頻繁に参照するので、そのシンボルが Rust の初期化処理 (prelude) でロードされるようになっており、このような省略記述が可能になっています。

Option 型の値を match で処理する

下記の関数は、引数で受け取った文字列を数値に変換し、Option<i32> 型の値として返しています。 つまり、Some<i32> あるいは None というバリアントを返します。 数値としてパースできない文字列が渡されたときは、None を返すようにしています(Rust 以外の言語であれば、null を返したり、例外を発生させたりするところです)。

/// 数値っぽい文字列を数値に変換します。
fn parse_num_str(s: &str) -> Option<i32> {
    match s {
        "one" | "一" => Some(1),
        "two" | "二" => Some(2),
        "three" | "三" => Some(3),
        _ => None,
    }
}

戻り値の Option<i32> は列挙型の値なので、次のように match で分岐処理しつつ、Some バリアントに含まれている i32 値を取り出すことができます(参考: 列挙型 (enum) )。

let num_opt = parse_num_str("三");

match num_opt {
    Some(num) => println!("The number is {}", num),
    None => println!("Could not parse"),
}

Option 型の値を if let で処理する

Option 型の値として Some バリアントが返された場合のみ何らかの処理をしたいときは、match の代わりに if let 構文を使うとシンプルに記述できます。

Some が返された場合のみ処理する
let num_opt = parse_num_str("三");
if let Some(num) = num_opt {
    // ここで num は i32 型の値になっている
    println!("The number is {}", num);
}

match 構文と同様に、特定のリテラル値に一致するかどうかを調べることもできます。

let num_opt = parse_num_str("三");
if let Some(3) = num_opt {
    println!("Found: three");
}

None かどうかをチェックする (is_none)

Option 列挙型の値が None バリアントかどうかを確認したいときは、is_none() メソッドを使うのがシンプルです。 値が存在しないときに早期リターンしたいケースで使えるかもしれません。 逆に Some バリアントかどうかを調べる is_some() も用意されていますが、あまり使うことはないでしょう。

let num_opt = parse_num_str("ほげ");
if num_opt.is_none() {
    eprintln!("Parse error");
    return;
}

// もちろん次のように書いても OK
// if let None = num_opt { ... }

Option 列挙型には、Some バリアントが保持するデータをダイレクトに取り出すための unwrap というメソッドが用意されていますが、このメソッドは None バリアントに対して呼び出すと panic が発生するので危険です。 ただ、上記のように None のケースを排除できていれば、安全に unwrap することができます。

// Some バリアントであることがわかっていれば unwrap で安全に値を取り出せる
let num = num.unwrap();
println!("The number is {}", num);

None だった場合に代替値を使う (unwrap_or, unwrap_or_else)

Some バリアントが保持するデータを取り出す unwrap メソッドは、None バリアントに対して呼び出すと panic が発生してしまう危険なメソッドですが、代わりに unwrap_or メソッドを使うと、None だった場合に代替値を返すことができます。 次の例では、get_user_id 関数が返す Option 値が None だった場合に、代替値として -1 を使うようにしています。

fn get_user_id(name: &str) -> Option<i32> {
    match name {
        "root" => Some(0),
        "maku" => Some(1),
        _ => None,
    }
}

let opt_id = get_user_id("unknown");
let id = opt_id.unwrap_or(-1);  // opt_id が None のとき -1 になる
println!("{}", id);  //=> -1

unwrap_or メソッドで指定する代替値は、メソッドの引数として渡すことになるので、そこに何らかの式を指定すると必ず評価されてしまうことに注意してください。

// get_default_id 関数は必ず呼び出されてしまう
let id = opt_id.unwrap_or(get_default_id());

この振る舞いを防ぐには、unwrap_or の代わりに unwrap_or_else メソッドを使用して、None 時に呼び出す関数を渡すようにします。 次の例では、関数名を指定する代わりに匿名関数(ラムダ式)を渡しています。

// opt_id が None のときのみ get_default_id 関数が呼び出される
let id = opt_id.unwrap_or_else(|| get_default_id());

似たようなメソッドに、unwrap_or_default がありますが、こちらは代替値としてその型のデフォルト値(i32 なら 0String なら ""Vec なら vec![])を返します。 場面によっては便利かもしれませんが、若干意図が伝わりにくい気がします。

let opt_id = get_user_id("unknown");
let id = opt_id.unwrap_or_default();  //=> 0

unwrap_or 系のメソッドを使ったコードは、次のような if let でも同様のことを行えることに気がつくかもしれません。 ただ、このようなコードは可読性が悪いので、unwrap_or 系のメソッドをうまく使いこなしたいところです。

let id = if let Some(id) = opt_id { id } else { -1 };

結局 Option 型の値はどうやってハンドルすればよいの?

以上のように、Option 型の値はいろいろなハンドル方法がありますが、どのようにハンドルするかは、次のような優先度で考慮すればよいと思います。

  1. matchSomeNone の両方のケースをハンドルする
    • matchOption のバリアントが包括的に処理されているかをコンパイル時に確認してくれるので安全です。
  2. if let でハンドルする
    • 特定の Some 値にしか興味がない場合は、if let でその値を取り出すことを考えます。ただし、想定外のバリアント値を見過ごすことがないように、else ブロックを配置しておくと安全です。
  3. unwrap_or 系のメソッドでハンドルする
    • データが存在しなかった場合にデフォルト値で済ませられる場合は、unwrap_or 系メソッドを使うと簡潔なコードになります。
  4. その他のメソッドでハンドルする
    • 十分に注意して Option 型のその他のメソッドを使用します。特に、panic を発生させる unwrap() メソッドなどは、プロダクトコードでは使わないようにするのが無難です。