C++erのRust入門2:エラー
Rust ガンガンやってくよー。
本当はZennのスクラップが使いたいんだけど、VSCodeからかけないのが難点。Emacsキーバインドがないと辛いんじゃ。
Rust でよくわからないのがエラー。これがほんとによくわからない。
自作エラーの定義方法
取りあえず、enum を定義して、std::error::Error を実装すればよいみたい。 std::error::Error は Debug と Display トレイトを実装しないといけないみたい。
ついでに、Result も自作の型を定義してることもあるみたいね。
エラー処理の ?
Result で返ってくるエラーは match や if let で処理する他に、 ? 演算子で自動的に早期 Return できる。
Box<dyn Error>
複数の種類のエラーを投げる際にざっくばらんに投げられるのが Box<dny Error> みたい。
これを使ってしまうと、なんのエラーなのか型から理解することが不可能になるため、実行してみないとわからないというデメリットがある。
したがって、多分使えるのは main 関数ぐらい。
? を使っても勝手にボクシングされるっぽい。
From で自動変換
? でなんで Box<dny Error> になるんじゃろって調べたら、 From トレイトを実装することで自動的に変換できるらしい。
From トレイトを実装すると、他には into とかが使えるようになる模様。
以下の例では、 MyError と std::io::Error を自動的に ? で MyError2 に変換している。
#[derive(Debug)]
pub enum MyError2 {
MyError(MyError),
IoError(std::io::Error),
}
impl std::error::Error for MyError2 { }
impl std::fmt::Display for MyError2 {
// ... 略 ...
}
impl From<MyError> for MyError2 {
fn from(x: MyError) -> Self { Self::MyError(x) }
}
impl From<std::io::Error> for MyError2 {
fn from(x: std::io::Error) -> Self { Self::IoError(x) }
}
pub fn piyo2() -> result::Result<u32, MyError2> {
bar(32)?;
let _ = std::fs::File::open("piyo")?;
Ok(1)
}
原因のエラーを返す
C++とかJavaなら、エラーを階層構造にして、原因を追求できる。
先の例では原因のエラーを MyError2 に保持しているから、せっかくだし、Rustにはそういう仕組はないのだろうか?と調べたら、なんかよくわからんけど、source という関数があるのを見つけた。
impl std::error::Error for MyError2 {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::MyError(x) => Some(x),
Self::IoError(x) => Some(x),
}
}
}
しかし、これでエラーの原因を取れると言っても、あまりRustではメリットなさそうだよね。エラーのスタックトレースとか取れるわけじゃないし…。(1.65.0でBacktraceの実装は提供されたけど、ErrorのBacktrace機能はまだっぽい)
ライフタイム境界
ところで、先のコードはなんかコンパイルが通ってしまったのだが、&(dyn Error + 'static) って何?Rust入門者なC++erなJava屋的にはグローバルスコープを持つErrorインターフェースを実装したクラスへの参照と読み取ったけど、意味わからん。
というわけで、調べて出てきたのがこちら。
ここでの 'static はライフタイムに対する境界で、 'static 以上のライフタイムになれなければならない等事っぽい。すなわち、グローバル変数として定義可能な型じゃないとだめだということで、実際に参照先が'static である必要はないんじゃないかな。参照先が 'static というのなら、たぶん &'static dyn Errorとかになるんだと思われる。
MyError や std::io::Error が特定のスコープの参照とかを持っていたらだめだけど、どちらもグローバル変数として定義可能な型だから問題ないということなのだ。
thiserror を使う
MyError2 は正直実装するのが面倒くさい。From とか、source とかいちいち書くの面倒くさすぎる。
そんなときは thiserror を使うと良いらしい。
cargo add thiserror
以下のようにすれば、先程の MyError2 と同等の実装(FromとsourceとDisplayの実装)が行える。Hoge は追加の実装してみた。
なるほど、これぐらいならちょっと頑張ってみようと思えるね。
use thiserror::Error;
#[derive(Error, Debug)]
pub enum MyError3 {
#[error("My Error: {0}")]
MyError(#[from] MyError),
#[error("Io Error: {0}")]
IoError(#[from] std::io::Error),
#[error("Hoge Error {a}, {b}")]
Hoge { a: u32, b: String },
}
pub fn piyo3() -> result::Result<u32, MyError3> {
bar(32)?;
let _ = std::fs::File::open("piyo")?;
Err(MyError3::Hoge {
a: 1,
b: "hoge".to_string(),
})
}
Backtrace
Rustの1.65 からスタックトレースっぽい Backtrace に関する機能が提供されるようになったようで、thiserror のページにもなにか書いてあるんだけどよくわからない。誰か教えて。
一応、Rust のバージョンをnightly にして、main.rs に以下を追加すると、Backtraceを保持させることは出来たけど、provide とかどう使うのかさっぱりわからん。
#![feature(provide_any)]
#![feature(error_generic_member_access)]
#[derive(Error, Debug)]
pub enum MyError4 {
#[error("4: My Error: {0}")]
MyError(#[from] MyError, Backtrace),
#[error("4: Io Error: {0}")]
IoError(#[from] std::io::Error, Backtrace),
#[error("4: Hoge Error {a}, {b}")]
Hoge { a: u32, b: String },
}
まとめ
なんとなくはわかったけど、Rustのエラー全然わからない。
C++とかJavaのエラーに比べて面倒くさいし、スタックトレースも今のところ表示されないから、エラー機能は貧弱だねぇ。
dyn std::error::Error で投げたエラーを C++ みたいに型ごとに match とかできれば楽なんだろうけど、それは出来ないみたいだし、やっぱりこのへんは Rust がしんどいと思う。
Rust 屋からは C++ のエラーも Rust と変わらん、と言われてんけど、やっぱり納得いかんわー。
Discussion