RustのOption型とResult型でエラーハンドリングをする
Rustでは、他の言語のようにnullや例外(Exception)が存在しません。 その代わりに、Rustは「値の存在有無をOption型」、「処理の成功・失敗をResult型」で扱います。また、回復不能なエラーが発生した場合にはpanic!でプログラムの実行を中止させることができます。
値の存在有無を表すOption<T>
型
Rustでは、値が存在するか存在しないかという概念をenumのOption<T>
型で定義しています。 Option<T>
型は、Some<T>
とNone
の列挙型として定義されます。Some<T>
は型T
の値を保持し、None
は値がないことを表します。これは、値が存在しない可能性がある値をより安全に扱うために、Rustのコードで一般的に使用されます。
※<T>
はジェネリックスと呼ばれ任意のデータ型を表します。よくわからない場合は、ジェネリックなデータ型をご覧ください。
pub enum Option<T> {
None, // 値が存在しないことを表す
Some(T) // `T`型の値が存在することを表す
}
Option<T>
型はとても有益なので、列挙子のSome
とNone
はOption::
という接頭辞なしに利用できます。
let some_number = Some(5);
let some_string = Some("a string");
let absent_number: Option<i32> = None;
Option<T>
型はどういうときに使うか
値が必ずしも存在しない可能性がある場合は、Option<T>
型が利用されます。
Option<T>
で値があるときに処理をしたい場合は、match
式と以下のように組み合わせることができる。
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
fn main() {
let five = Some(5);
let six = plus_one(five); // Some(6)
let none = plus_one(None); // None
}
また、if let
で値があるときだけなにか処理をしたいという制御フローを簡潔に書くことができます。
fn main() {
let some_u8_value = Some(0u8);
// matchで書いたパターン
match some_u8_value {
Some(3) => println!("three"),
_ => (),
}
// if let で書き換えたパターン
if let Some(3) = some_u8_value {
println!("three");
}
}
Option<T>
型のよく使う便利メソッド
unwrap()
やexpect()
で値を取得する
unwrap
やexpect
を使うことでOption<T>
の値がSome
の場合は値を取得し、None
の場合はパニックを起こすことができます。値がないときにパニックが起きるので、テストコードやサンプルコードなどでよく使われがちです。本番コードではmatch
や?
などでハンドリングしたほうがよいです。
fn main() {
let some_number = Some(5);
assert_eq!(some_number.unwrap(), 5);
// let absent_number: Option<i32> = None;
// absent_number.unwrap(); // 値がNoneなのでパニックが起きる
let absent_number: Option<i32> = None;
absent_number.unwrap(); // 値がNoneなのでパニックが起きる
let absent_number: Option<i32> = None;
absent_number.expect("custom panic message"); // 値がNoneなのでカスタムメッセージでパニックが起きる
}
?
によるアンパックする
Option
の値を取得するためにmatch
を使うこともできますが、?
を使うほうがコードの可読性が高まります。?
は評価した値がSome
の場合は値を取得し、None
の場合は実行中の関数を終了させNone
を返します。制約として、関数がOption<T>
を返す必要があります。
fn main() {
assert_eq!(add_plus_one(Some(2)), Some(3));
assert_eq!(add_plus_one(None), None);
}
fn add_plus_one(x: Option<i32>) -> Option<i32> {
let v = x? + 1; // x? が Noneの場合は、ここで早期リターンされる
Some(v)
}
take()
で値を取り出す
take()
で値を取り出し、None
を設定します。
fn main() {
let mut x = Some(2);
// xから値を取り出しyに代入し、代わりにNoneにする
let y = x.take();
assert_eq!(x, None);
assert_eq!(y, Some(2));
let mut x: Option<u32> = None;
// xから値を取り出しyに代入し、代わりにNoneにする
let y = x.take();
assert_eq!(x, None);
assert_eq!(y, None);
}
map
で値を変換する
map()
は値に関数を適用して、Option<T>
を Option<U>
に変換することができます。メソッドチェインが必要なときにmap
を使うことで、可読性を高めることができます。
fn main() {
let some_string = Some("a string".to_string());
// map は Option<T> から Option<U> への変換を行う
let some_length = some_string.map(|s| s.len());
assert_eq!(some_length, Some(8));
// None の場合は Noneを返す
let none_length: Option<usize> = None.map(|s: String| s.len());
assert_eq!(none_length, None);
}
他にもOption<T>
には便利なメソッドがあります。より詳しく知りたい場合は、Option in std::option - Rustを確認してみてください。
処理の成功・失敗を表すResult
型
Result<T, E>
型は、Ok<T>
とErr<E>
の2つ列挙子を持つ列挙型です。Okは、成功した操作を表す型Tの値を保持し、Errはエラーを表す型Eの値を保持します。この型は、成功するかどうか不確かな値を返す場合に役立ちます。
pub enum Result<T, E> {
Ok(T), // 成功した値が含まれる
Err(E), // エラーの値が含まれる
}
Result
型はどういうときに使うか
Resut<T, E>
型では、処理が成功したか失敗したかといったエラーハンドリングをします。 Resut<T, E>
を使うことで、例外を起こさずにデータ型にもとづいてエラーハンドリングができるので、エラーハンドリングが実装されているかどうかをコンパイラにチェックさせることができます。
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
let _f = match f {
// ファイルがある場合は、そのファイルを返す
Ok(file) => file,
// ファイルがない場合は、パニックを起こす
Err(error) => {
panic!("There was a problem opening the file: {:?}", error);
}
};
}
Result
型のよく使う便利メソッド
Result
型にも、Option<T>
型と同じような便利なメソッドが定義されています。
?
で値を取得する
?
演算子は、Result
の値がOk
の場合はOk
の中身の値が返ってきます。一方、Result
の値がErr
の場合は早期リターンをしてErr
を返します。?
演算子を使うことで、エラーハンドリングのコードが読みやすくなります。?
演算子の制約としては、戻り値にResult<T, E>
を持つ関数やメソッドでしか利用できません。
use std::fs::File;
use std::io;
use std::io::Read;
fn main() {
let s = read_username_from_file();
match s {
Ok(s) => println!("s is {}", s),
Err(e) => println!("e is {:?}", e),
}
}
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?; // ファイルがない場合早期リターンでErrを返す
let mut s = String::new();
f.read_to_string(&mut s)?; // ファイルの読み込みに失敗した場合、早期リターンでErrを返す
Ok(s)
}
unwrap()
やexpect()
で値を取得する
unwrap
やexpect
を使うことでResult<T, E>
の値がOk(T)
の場合はT
型の値を取得し、Err<E>
の場合はパニックを起こすことができます。値がないときにパニックが起きるので、テストコードやサンプルコードなどでよく使われがちです。本番コードではmatch
や?
などでエラーハンドリングをしたほうがよいです。
fn main() {
// ファイルがない場合はパニックが呼ばれ、ファイルがあればファイルが返される
let f = File::open("hello.txt").unwrap();
// ファイルがない場合は"Failed to open hello.txt"というメッセージでパニックが表示され、
// ファイルがあればファイルが返される
let f = File::open("hello.txt").expect("Failed to open hello.txt");
}
map
で値をマップする
map()
は値に関数を適用して、Result<T, E>
を Result<U, E>
に変換します。値がErr
の場合は関数は適用されません。
fn main() {
// Okなので、値が2倍されて"200"が出力される
match "100".parse::<i32>().map(|i| i * 2) {
Ok(n) => println!("{n}"),
Err(..) => println!("parse error"),
}
// Errなので、"parse error"が出力される
match "invalid".parse::<i32>().map(|i| i * 2) {
Ok(n) => println!("{n}"),
Err(..) => println!("parse error"),
}
}
Result<T, E>
型のより詳しいメソッドを知りたい場合は、Result in std::result - Rustを眺めてみてください。
実行を中止させるpanic!
panic!
マクロを使うことで、現在のスレッドをパニックにし、プログラムはすぐに終了します。
fn main() {
panic!("crash and burn"); // ここでプログラムが終了する
println!("Do something");
}
panic!
はどういうときに使うか
サンプルコードやテストで条件をアサートするのに利用すると便利です。特に、これらのケースでのパニックはunwrap()
やexpect()
で利用されることが多いでしょう。
また、panic!
はプログラムで検出された回復不能なバグを表すエラーを作成されるために使用することもあります。
Rustのエラーハンドリングまとめ
Rustのエラーハンドリングの機能のまとめです。
- RustにはNullや例外(Exception)の機能がない。
- Nullの代わりに、値があるなしは
Option
型を使う - 例外(Exception)の代わりに、処理が成功か失敗したかに
Result
型を使う - 回復不能なエラーが発生した場合は、
panic!
でプログラムの実行を停止させる