はじめに
Rustを書いてると避けて通れないのが Result<T, E> と Option<T>。
最初は「両方とも『値があるかないか』を表すんでしょ?何が違うの?」って思ってました。
全然違った。
この記事では、この2つの使い分けを整理します。
目次
それぞれの定義
Option
enum Option<T> {
Some(T), // 値がある
None, // 値がない
}
「値があるかもしれないし、ないかもしれない」 を表す。
Result
enum Result<T, E> {
Ok(T), // 成功
Err(E), // 失敗(エラー情報付き)
}
「成功したか、失敗したか(失敗なら理由付き)」 を表す。
使い分けの基準
Option を使う場面
「ないこと」が正常なケースで、エラーではない
fn find_user(id: u32) -> Option<User> {
// ユーザーが見つからないのは「エラー」ではない
// 単に「いない」だけ
}
fn first_element<T>(vec: &[T]) -> Option<&T> {
// 空のベクタに「最初の要素」はない
// でもそれは正常な状態
vec.first()
}
Optionが適切な例
- コレクションの検索(見つからないかもしれない)
- パースの結果(値がないかもしれない)
- デフォルト値があるかどうか
- オプショナルな設定項目
Result を使う場面
「失敗」であり、その理由を伝えたい
fn read_file(path: &str) -> Result<String, std::io::Error> {
// ファイルが読めないのは「エラー」
// 理由(パーミッション、存在しない等)を伝えたい
std::fs::read_to_string(path)
}
fn parse_number(s: &str) -> Result<i32, std::num::ParseIntError> {
// パースに失敗したら「エラー」
// 何が悪かったのか知りたい
s.parse()
}
Resultが適切な例
- ファイル操作(I/Oエラーの可能性)
- ネットワーク通信(接続エラーの可能性)
- パース処理(不正な入力の可能性)
- データベース操作(クエリエラーの可能性)
迷ったときのフローチャート
「値がない」状態は...
│
├─ 正常な状態である → Option
│ 例:空のリストの最初の要素
│
└─ 異常な状態(エラー)である → Result
│
└─ エラーの理由を伝えたいか?
├─ Yes → Result<T, カスタムError>
└─ No → Result<T, ()> か Option
よく使うメソッド
Option のメソッド
unwrap系
let x: Option<i32> = Some(5);
x.unwrap(); // Some → 値、None → パニック
x.unwrap_or(0); // Some → 値、None → デフォルト値
x.unwrap_or_else(|| compute_default()); // None時に関数実行
x.unwrap_or_default(); // None → T::default()
x.expect("エラーメッセージ"); // Noneでパニック(メッセージ付き)
変換系
let x: Option<i32> = Some(5);
x.map(|n| n * 2); // Some(5) → Some(10)
x.and_then(|n| Some(n * 2)); // flatMap的な
x.filter(|n| *n > 3); // 条件を満たさないとNone
判定系
let x: Option<i32> = Some(5);
x.is_some(); // true
x.is_none(); // false
Result のメソッド
unwrap系
let x: Result<i32, &str> = Ok(5);
x.unwrap(); // Ok → 値、Err → パニック
x.unwrap_or(0); // Ok → 値、Err → デフォルト値
x.unwrap_err(); // Err → エラー値、Ok → パニック
x.expect("失敗した理由"); // Errでパニック(メッセージ付き)
変換系
let x: Result<i32, &str> = Ok(5);
x.map(|n| n * 2); // Ok(5) → Ok(10)
x.map_err(|e| format!("Error: {}", e)); // エラーを変換
x.and_then(|n| Ok(n * 2)); // flatMap的な
判定系
let x: Result<i32, &str> = Ok(5);
x.is_ok(); // true
x.is_err(); // false
変換メソッド
Option → Result
let opt: Option<i32> = Some(5);
// Noneの場合のエラー値を指定
let result: Result<i32, &str> = opt.ok_or("値がない");
// 遅延評価版
let result: Result<i32, String> = opt.ok_or_else(|| format!("値がない"));
Result → Option
let result: Result<i32, &str> = Ok(5);
// Errを捨ててOptionに
let opt: Option<i32> = result.ok();
// Okを捨ててエラーをOptionに
let err_opt: Option<&str> = result.err();
使用例
fn find_and_process(id: u32) -> Result<String, MyError> {
// find_userはOption<User>を返す
// ok_orでResult<User, MyError>に変換
let user = find_user(id)
.ok_or(MyError::UserNotFound(id))?;
Ok(process(user))
}
?演算子
?演算子は、エラーハンドリングを楽にする神機能。
Result での使い方
fn read_config() -> Result<Config, std::io::Error> {
// ?をつけると、Errの場合は早期リターン
let content = std::fs::read_to_string("config.toml")?;
let config = parse_config(&content)?;
Ok(config)
}
// 上は以下と同じ
fn read_config_verbose() -> Result<Config, std::io::Error> {
let content = match std::fs::read_to_string("config.toml") {
Ok(c) => c,
Err(e) => return Err(e),
};
let config = match parse_config(&content) {
Ok(c) => c,
Err(e) => return Err(e),
};
Ok(config)
}
Option での使い方
fn get_nested_value(data: &Data) -> Option<i32> {
let a = data.get_a()?; // Noneなら早期リターン
let b = a.get_b()?;
let c = b.get_c()?;
Some(c.value)
}
エラー型の変換
?を使うとき、エラー型が異なると変換が必要:
use std::num::ParseIntError;
use std::io;
#[derive(Debug)]
enum MyError {
Io(io::Error),
Parse(ParseIntError),
}
impl From<io::Error> for MyError {
fn from(e: io::Error) -> Self {
MyError::Io(e)
}
}
impl From<ParseIntError> for MyError {
fn from(e: ParseIntError) -> Self {
MyError::Parse(e)
}
}
fn process() -> Result<i32, MyError> {
let content = std::fs::read_to_string("number.txt")?; // io::Error → MyError
let num: i32 = content.trim().parse()?; // ParseIntError → MyError
Ok(num)
}
実践的なパターン
パターン1:連鎖的な処理
fn get_user_email(user_id: u32) -> Option<String> {
find_user(user_id)
.and_then(|user| user.email)
.map(|email| email.to_lowercase())
}
パターン2:デフォルト値の処理
fn get_config_value(key: &str) -> String {
config.get(key)
.cloned()
.unwrap_or_else(|| default_config(key))
}
パターン3:複数のOptionを組み合わせ
fn calculate(a: Option<i32>, b: Option<i32>) -> Option<i32> {
Some(a? + b?) // どちらかがNoneならNone
}
// または
fn calculate_alt(a: Option<i32>, b: Option<i32>) -> Option<i32> {
match (a, b) {
(Some(x), Some(y)) => Some(x + y),
_ => None,
}
}
パターン4:ログを出しつつ処理
fn process_with_logging(data: Option<Data>) -> Option<Result<Output, Error>> {
data.map(|d| {
println!("Processing: {:?}", d);
process(d)
})
}
パターン5:イテレータとの組み合わせ
fn sum_valid_numbers(inputs: &[&str]) -> i32 {
inputs.iter()
.filter_map(|s| s.parse::<i32>().ok()) // パース成功したものだけ
.sum()
}
よくある間違い
❌ unwrap の乱用
// 悪い例
fn get_data() -> Data {
fetch_data().unwrap() // パニックする可能性
}
// 良い例
fn get_data() -> Result<Data, Error> {
fetch_data() // エラーを呼び出し元に伝播
}
❌ Option を Result として使う
// 悪い例:エラーの理由がわからない
fn parse_config(s: &str) -> Option<Config> {
// パースに失敗した理由を伝えられない
}
// 良い例
fn parse_config(s: &str) -> Result<Config, ParseError> {
// エラーの詳細を返せる
}
❌ 無駄な match
// 悪い例
let value = match opt {
Some(v) => v,
None => default,
};
// 良い例
let value = opt.unwrap_or(default);
まとめ
使い分け早見表
| 状況 | 使うべき型 |
|---|---|
| 値がないのが正常 | Option<T> |
| 値がないのがエラー | Result<T, E> |
| エラーの理由を伝えたい | Result<T, E> |
| 検索で見つからない | Option<T> |
| ファイルI/O | Result<T, io::Error> |
| パース処理 | Result<T, ParseError> |
チェックリスト
-
unwrap()を使う前に本当に安全か考える -
エラーの理由を伝えるべきなら
Resultを使う -
?演算子を活用してコードを簡潔に -
map,and_then,unwrap_orを使いこなす
今すぐできるアクション
- 既存コードの
unwrap()を見直す -
OptionとResultの変換メソッドを覚える -
?演算子を使ってエラーハンドリングを簡潔にする
Result と Option、最初は混乱するけど、「エラーかどうか」で考えると使い分けられるようになります。
みなさんも良いRustライフを!
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!