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