❓
Option<T> — nilが消えた世界で生き残る
Rubyのnilと&.演算子がRustでどう変わるか
Rubyではnilは空気のような存在だ。配列の存在しないインデックスを参照するとnil、Hashの存在しないキーを参照するとnil、メソッドが明示的に返さなければnil。そしてnilにメソッドを呼ぶとNoMethodError。本番エラーのかなりの部分がここから来る。
Rustはnilを完全に排除した。代わりにOption<T>というenumを使う。
enum Option<T> {
Some(T), // 値がある
None, // 値がない
}
Rubyとの比較
# Ruby
def find_user(id)
users[id] # nilかもしれない
end
user = find_user(99)
user.name # nilならNoMethodError!
// Rust
fn find_user(id: u32) -> Option<User> {
// Some(user)またはNone
}
let user = find_user(99);
// user.name // コンパイルエラー!Option<User>にnameメソッドはない
RustはOption<User>とUserを別の型として扱う。Optionから値を取り出して初めて使える。
値の取り出し — unwrap(危険)
let user = find_user(99).unwrap(); // Noneならpanic!
Rubyでnilにメソッドを呼ぶのと同程度の危険。本番コードでは避けるべき。
安全な取り出し — match, if let, map
// match
match find_user(99) {
Some(user) => println!("{}", user.name),
None => println!("ユーザーなし"),
}
// if let(1つだけチェック)
if let Some(user) = find_user(99) {
println!("{}", user.name);
}
// map — Rubyの&.に最も近い
let name = find_user(99).map(|u| u.name);
// Option<String>を返す
&. → map/and_then 対応表
Rubyのsafe navigationとOptionメソッドの対応:
# Ruby
user&.name # nilならnil返却
user&.address&.city # チェーン
user&.name || "unknown" # デフォルト値
// Rust
user.map(|u| u.name) // NoneならNone
user.and_then(|u| u.address).map(|a| a.city) // チェーン
user.map(|u| u.name).unwrap_or("unknown") // デフォルト値
mapはSome内の値を変換する。and_thenは変換結果がまたOptionの場合に使う(flatMap)。unwrap_orはNoneの時のデフォルト値。
unwrap_or_else — デフォルト値が高コストな場合
let name = find_user(99)
.map(|u| u.name)
.unwrap_or_else(|| fetch_default_name()); // Noneの時だけ実行
Rubyの||(user&.name || expensive_default)は右辺を必要な時だけ評価する。Rustのunwrap_or_elseも同じ遅延評価。
キーポイント
1
Option<T>はSome(値)またはNone — nilの型安全な代替
2
unwrapは危険 — match、if let、mapで安全に処理
3
&. → map、チェーン → and_then、デフォルト値 → unwrap_or
4
Option<User>とUserは別の型 — 必ず取り出して使用
メリット
- ✓ nilに対するNoMethodErrorがそもそも不可能 — コンパイル時に検出
- ✓ None処理を漏らすとコンパイルが通らない
デメリット
- ✗ Rubyのようにnilを無視して進めないので最初は面倒
- ✗ Optionチェーンが長くなると可読性が落ちることがある
ユースケース
DBクエリ結果が存在しない可能性がある時 — find vs find_by
設定値が存在するかもしれないし存在しないかもしれない時