🦀

C++erのRust入門2:エラー

2023/05/01に公開

Rust ガンガンやってくよー。

本当はZennのスクラップが使いたいんだけど、VSCodeからかけないのが難点。Emacsキーバインドがないと辛いんじゃ。

Rust でよくわからないのがエラー。これがほんとによくわからない。

自作エラーの定義方法

取りあえず、enum を定義して、std::error::Error を実装すればよいみたい。 std::error::ErrorDebugDisplay トレイトを実装しないといけないみたい。

ついでに、Result も自作の型を定義してることもあるみたいね。

https://github.com/nodamushi/zenn-program/blob/a992522546bb5703ed9da91a5a601bdebc58f966/src/rust/study/err/src/myerror.rs#L4-L20

エラー処理の ?

Result で返ってくるエラーは matchif let で処理する他に、 ? 演算子で自動的に早期 Return できる。

https://github.com/nodamushi/zenn-program/blob/a992522546bb5703ed9da91a5a601bdebc58f966/src/rust/study/err/src/myerror.rs#L30-L38

Box<dyn Error>

複数の種類のエラーを投げる際にざっくばらんに投げられるのが Box<dny Error> みたい。
これを使ってしまうと、なんのエラーなのか型から理解することが不可能になるため、実行してみないとわからないというデメリットがある。

したがって、多分使えるのは main 関数ぐらい。

https://github.com/nodamushi/zenn-program/blob/a992522546bb5703ed9da91a5a601bdebc58f966/src/rust/study/err/src/myerror.rs#L40-L44

? を使っても勝手にボクシングされるっぽい。

From で自動変換

? でなんで Box<dny Error> になるんじゃろって調べたら、 From トレイトを実装することで自動的に変換できるらしい。

From トレイトを実装すると、他には into とかが使えるようになる模様。

以下の例では、 MyErrorstd::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インターフェースを実装したクラスへの参照と読み取ったけど、意味わからん。

というわけで、調べて出てきたのがこちら。

https://laysakura.github.io/2020/05/21/rust-static-lifetime-and-static-bounds/

ここでの 'static はライフタイムに対する境界で、 'static 以上のライフタイムになれなければならない等事っぽい。すなわち、グローバル変数として定義可能な型じゃないとだめだということで、実際に参照先が'static である必要はないんじゃないかな。参照先が 'static というのなら、たぶん &'static dyn Errorとかになるんだと思われる。

MyErrorstd::io::Error が特定のスコープの参照とかを持っていたらだめだけど、どちらもグローバル変数として定義可能な型だから問題ないということなのだ。

thiserror を使う

MyError2 は正直実装するのが面倒くさい。From とか、source とかいちいち書くの面倒くさすぎる。

そんなときは thiserror を使うと良いらしい。

cargo add thiserror

以下のようにすれば、先程の MyError2 と同等の実装(FromsourceDisplayの実装)が行える。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