Rustのエラーハンドリング入門:Result・Option・?演算子の使い方
はじめに
Rustには例外(Exception)がありません。代わりに Result<T, E> と Option<T> という型でエラーや値の有無を表現します。コンパイラがエラーの見落としを防いでくれるため、実行時の予期しないクラッシュを大幅に減らせます。
Option:値があるかないか
Option<T> は値が存在するかどうかを表します。
enum Option<T> { Some(T), // 値がある None, // 値がない }
Vec::get や HashMap::get など、値が見つからない可能性があるメソッドが Option を返します。
let v = vec![1, 2, 3]; let first = v.get(0); // Some(&1) let tenth = v.get(9); // None
Option の扱い方
let v = vec![1, 2, 3]; let val = v.get(0); // match で分岐 match val { Some(x) => println!("値: {}", x), None => println!("見つかりません"), } // if let(None を無視したい場合) if let Some(x) = val { println!("値: {}", x); } // unwrap(None ならパニック。テスト用途向き) let x = val.unwrap(); // unwrap_or(None のときのデフォルト値) let x = val.unwrap_or(&0); // unwrap_or_else(None のときに関数を実行) let x = val.unwrap_or_else(|| &99); // ?演算子(None なら早期リターン) fn first_element(v: &Vec<i32>) -> Option<&i32> { let x = v.get(0)?; // None なら関数を抜ける Some(x) }
Option のメソッド
let some: Option<i32> = Some(5); let none: Option<i32> = None; some.is_some() // true some.is_none() // false some.map(|x| x * 2) // Some(10) none.map(|x| x * 2) // None some.filter(|x| *x > 3) // Some(5) some.filter(|x| *x > 9) // None some.or(Some(99)) // Some(5) none.or(Some(99)) // Some(99) some.unwrap_or(0) // 5 none.unwrap_or(0) // 0
Result<T, E>:成功か失敗か
Result<T, E> は処理が成功したか失敗したかを表します。
enum Result<T, E> { Ok(T), // 成功(値 T を持つ) Err(E), // 失敗(エラー E を持つ) }
ファイル操作やパースなど、失敗する可能性のある処理が Result を返します。
use std::fs; let result = fs::read_to_string("hello.txt"); // Ok(String) または Err(io::Error)
Result の扱い方
use std::fs; let result = fs::read_to_string("hello.txt"); // match で分岐 match result { Ok(content) => println!("{}", content), Err(e) => eprintln!("エラー: {}", e), } // if let if let Ok(content) = fs::read_to_string("hello.txt") { println!("{}", content); } // unwrap(Err ならパニック) let content = fs::read_to_string("hello.txt").unwrap(); // expect(パニック時のメッセージを指定) let content = fs::read_to_string("hello.txt") .expect("ファイルが読み込めません"); // unwrap_or(Err のときのデフォルト値) let content = fs::read_to_string("hello.txt") .unwrap_or_else(|_| String::from("デフォルト"));
Result のメソッド
let ok: Result<i32, &str> = Ok(5); let err: Result<i32, &str> = Err("失敗"); ok.is_ok() // true ok.is_err() // false ok.map(|x| x * 2) // Ok(10) err.map(|x| x * 2) // Err("失敗") ok.map_err(|e| format!("エラー: {}", e)) // Ok(5) err.map_err(|e| format!("エラー: {}", e)) // Err("エラー: 失敗") ok.unwrap_or(0) // 5 err.unwrap_or(0) // 0 ok.ok() // Some(5)(Result → Option に変換)
? 演算子:エラーを早期リターン
関数内で ? を使うと、Err のとき自動で return Err(...) します。エラーを伝播させるコードがすっきり書けます。
use std::fs; use std::io; // ? なし(冗長) fn read_file_verbose(path: &str) -> Result<String, io::Error> { let content = match fs::read_to_string(path) { Ok(c) => c, Err(e) => return Err(e), }; Ok(content) } // ? あり(すっきり) fn read_file(path: &str) -> Result<String, io::Error> { let content = fs::read_to_string(path)?; Ok(content) }
複数のエラーを ? で繋げる
use std::fs; use std::io; fn read_and_parse(path: &str) -> Result<i32, Box<dyn std::error::Error>> { let content = fs::read_to_string(path)?; // io::Error かもしれない let number: i32 = content.trim().parse()?; // ParseIntError かもしれない Ok(number) }
Box<dyn std::error::Error> を使うと異なる型のエラーをまとめて扱えます。
unwrap・expect・? の使い分け
| 方法 | 使う場面 |
|---|---|
unwrap() |
テストコード・プロトタイプ(失敗しないと確信できる場合) |
expect("msg") |
失敗が設計上あり得ない場所(メッセージで原因を明示) |
? 演算子 |
エラーを呼び出し元に伝播したい場合(本番コードの基本) |
match / if let |
エラーごとに異なる処理をしたい場合 |
unwrap_or |
デフォルト値で代替できる場合 |
main関数でのエラーハンドリング
main も Result を返せます。? を使ったエラー処理がそのまま書けます。
use std::fs; fn main() -> Result<(), Box<dyn std::error::Error>> { let content = fs::read_to_string("hello.txt")?; println!("{}", content); Ok(()) }
エラーが発生すると標準エラー出力にメッセージが表示されます。
まとめ
| 型 | 使い方 |
|---|---|
Option<T> |
値があるかないか(null の代わり) |
Result<T, E> |
成功か失敗か(例外の代わり) |
Some(x) / None |
Option の2つの状態 |
Ok(x) / Err(e) |
Result の2つの状態 |
unwrap() |
値を取り出す(失敗時パニック) |
unwrap_or(default) |
失敗時はデフォルト値 |
? 演算子 |
エラーを呼び出し元に伝播 |
map(f) |
値を変換する(エラーはそのまま) |
Rustのファイル読み書きで ? を実際に使う例は「Rustのファイル読み書き入門:fs・BufReader・BufWriterの使い方」を参照してください。
Rustのイテレータについては「Rustのイテレータ入門:map・filter・collectの使い方」を参照してください。