|
Errorトレイトは、独自のエラー列挙型を作ったときに実装しておいた方がいいトレイトらしい。
Errorと名の付く構造体や型はあちこちにあるので、使うときはstd::error::Errorというように完全修飾した方が良さそう。
独自のエラー列挙型を作ってみる。
発生するエラーの種類に応じて列挙子を増やしていく。
まずはエラーメッセージだけを保持する列挙子のみで作ってみる。
#[derive(Debug)] pub enum MyError { MyMessageError(String), }
デバッグ出力できると便利(println!()の書式の{:?}
が使える)なので、#[derive]でDebugのコードを自動生成しておく。
独自のエラー列挙型を作ったときは、std::error::Errorを実装しておいた方がいいらしい。
std::error::Errorを実装するときは、先にDisplayトレイトを実装しておいた方が便利らしい。
impl std::fmt::Display for MyError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { MyError::MyMessageError(message) => write!(f, "my error message={}", message), } } }
impl std::error::Error for MyError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { MyError::MyMessageError(_message) => None, } } }
sourceメソッドは、エラーの原因となったエラー(Javaの例外のcause相当)があるときに、それを返すようだ。
今の例のMyMessageErrorにはエラー原因は無いので、Noneを返しておく。
fn main() { let result = example(); if let Err(my_error) = result { println!("{}", my_error); // MyErrorはDisplayを実装してあるので、println!()の書式の{}が使用できる } } fn example() -> Result<(), MyError> { return Err(MyError::MyMessageError("my error test".to_string())); }
↓実行例
my error message=my error test
std::io::Errorが発生すると想定して、それに対する列挙子を追加する。
#[derive(Debug)] pub enum MyError { MyMessageError(String), MyIoError(std::io::Error), }
列挙型に列挙子を増やしたので、Displayやstd::error::Errorのmatch式にも列挙子を追加する。
impl std::fmt::Display for MyError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { MyError::MyMessageError(message) => write!(f, "my error message={}", message), MyError::MyIoError(error) => write!(f, "I/O error={}", error), } } }
impl std::error::Error for MyError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { MyError::MyMessageError(_message) => None, MyError::MyIoError(error) => Some(error), // 元となったstd::io::ErrorをSomeに入れて返す } } }
それと、Fromトレイトを実装しておくと、std::io::Errorから自分独自のエラー列挙型に自動的に変換してくれるようになる。
impl From<std::io::Error> for MyError { fn from(value: std::io::Error) -> Self { MyError::MyIoError(value) } }
fn main() { let result = example(); if let Err(my_error) = result { println!("{}", my_error); } } fn example() -> Result<(), MyError> { let _s = std::fs::read_to_string("not-exists.txt")?; // read_to_string()のエラーの型はstd::io::Errorであり、 // exampleメソッドが返すエラーの型はMyErrorなので、一致していないが、 // Fromトレイトを実装してあるので、自動的に変換される Ok(()) }
↓実行例
I/O error=指定されたファイルが見つかりません。 (os error 2)
自前でエラーの列挙子を変換するなら、以下のようにResultのmap_errメソッドを使う。
fn example() -> Result<(), MyError> { let _s = std::fs::read_to_string("not-exists.txt").map_err(|e| MyError::MyIoError(e))?; Ok(()) }
Rustでは、ResultによってErrを返しても、スタックトレース(エラー発生個所・メソッド呼び出しのツリー)のような情報は付加されない。[2024-10-09]
しかし標準マクロのfile!()・line!()・column!()を使うとソースコード内の位置情報が取れるので、これをエラーメッセージに含めることは出来る。
エラーの列挙子インスタンスを作る箇所で毎回これらのマクロを呼ぶのも面倒なので、列挙子インスタンスを作るマクロも用意してみる。
#[derive(Debug)] pub enum MyError { ExampleError(String), } macro_rules! new_example_error { // MyError::ExampleErrorインスタンスを生成するマクロ ($message:expr) => { $crate::MyError::ExampleError(format!( "[{}:{}:{}] {}", file!(), line!(), column!(), $message )) }; }
fn main() { my_func().unwrap(); // ←行番号19 } fn my_func() -> Result<(), MyError> { return Err(new_example_error!("error example")); // ←行番号23 }
↓実行例
thread 'main' panicked at src/main.rs:19:15: ←これはunrwap()によってパニックが発生した場所 called `Result::unwrap()` on an `Err` value: ExampleError("[src/main.rs:23:16] error example") ←生成したエラーメッセージ
Resultの方も独自の型を定義しておいて、メソッドの返り値の型にそれを使うということもよく行われるようだ。
pub type MyResult<T> = Result<T, MyError>;
fn example() -> MyResult<()> { let _s = std::fs::read_to_string("not-exists.txt").map_err(|e| MyError::MyIoError(e))?; Ok(()) }