LoginSignup
5
4

More than 3 years have passed since last update.

[Rust]自作Errorを作る

Last updated at Posted at 2020-09-29

前回の記事

前回「[Rust]自作Errorを作りたくてstd::io::Errorを読んでみた!」という記事を書きました。

今回は実際に作って行こうと思います。実践編って感じです。

一応ですが、これは私のやり方であって、正解というわけでも、どこかの誰かが推奨しているわけでもありません。なのでもっといいやり方は自分で開発してくださいな。

課題1

エラー内容(&'static str)を表すError型を作成すること。

また、この型が表すエラーは他のエラーから連鎖的に発生する可能性があるものとする。

Step1;「他のエラーに誘起されるか」

答えはYesです。連鎖的に発生する可能性があるのですから。。

ということで、自作Errorには、他のエラーを入れれるようにしないといけません。ですが誘起されてではなく、自然発生(?)することもありえそうです。

そこで列挙型を用いて次のようにします。

use std::error;

pub struct Error {
    _error: _Error,
}

impl Error {
    // 他のエラー型を受け取れるようにコンストラクタを作っておく
    fn new<E>(kind: ?, error: E) -> Self
        where
            E: Into<Box<dyn error::Error + Send + Sync>>
    {
        Error{ _error: _Error::Custom((kind, error.into())) }
    }
}

enum _Error {
    Simple( ? ),
    Custom( (?, Box<dyn error::Error + Send + Sync>) ),
}

将来、突っ込まれるであろう他のエラーの型なんぞ、さっぱりわかりません。しかしエラーを表す型ならBox<dyn std::error::Error + Send + Sync>Intoできます(詳細は前回の記事へ)。そのためタプルを用いて、上記のようにします。

補足:構造体で作るメリット

ここでは構造体で列挙型を包む形を取っています。なぜかというと中身を見せないためです。

次のように定義したと仮定しましょう。

pub enum Error {
    Simple( ? ),
    Custom( (?, Box<dyn std::error::Error + Send + Sync>) ),
}

これでも上手く自作Errorが作れそうですよね?

しかし、これだと問題があります。それはPublicな列挙型は中身が見えるということです。

つまり、誰かがError::Simple(ほにゃほにゃ)して予期しない使い方をされたり、隠蔽できてないことから使い方を誤解させたりしかねません。

そのため中身を見せないように、ひと手間踏んで、隠蔽してるのです。

Step2;「エラーはどんな種類があるか?」

今回の課題でははっきりしてます。&'static strオンリーです。

ということで、先程の?&'static strで埋めます。

enum _Error {
    Simple( &'static str ),
    Custom( (&'static str, Box<dyn error::Error + Send + Sync>) ),
}

はい!ほぼほぼ完成!あとはerror::ErrorDisplayDebugを実装すればいいだけ!

Step3;「必要なトレイトを実装していく」

まずDisplayDebugから。今回は文字列を内部に持っているので、どちらも、それを表示させたいと思います。

use std::fmt;

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self._error {
            _Error::Simple(s) => f.write_str(s),
            _Error::Custom(c) => f.write_str(c.0),
        }
    }
}

impl fmt::Debug for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        <Self as fmt::Display>::fmt(self, f)
    }
}

私は「この後に実装するsourceメソッドで、発生元のエラーを取得できるのだから、このエラーの内容を表示させればよい」と考えこうしています。

しかし、連鎖元の内容を出力させたいとか、もっと意外性にとんだ人を魅了するデバック出力がしたいという人とかがいるかと思います。

そういう人は適宜書き換えましょう。大切なのはデバックしやすいということですから。

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self._error {
            _Error::Simple(s) => f.write_str(s),
            _Error::Custom(c) => c.1.fmt(f),
        }
    }
}

impl fmt::Debug for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use std::f64::consts::PI;
        f.write_fmt(format_args!("{}", (PI / PI) ))
    }
}

次にerror::Errorトレイトを実装します。今回は連鎖してくる可能性があるのでsourceメソッドを忘れずに書いておきます。

use std::error;

impl error::Error for Error {
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
        match &self._error {
            _Error::Simple(_) => None,
            _Error::Custom(c) => c.1.source(),
        }
    }
}

Step4;「他の必要なメソッド等を書く」

最後に他のメソッドも実装して終わりです。コンストラクタとか。

コンストラクタであればFrom<&'static str>とかでさくっと適宜つくっちゃってくださいな。

課題2

図書館の蔵書の検索システムに使われるError型を作ると想定します。

基本的には「蔵書を発見できなかった」、「蔵書を発見できたが利用不可能な状態だった」、「利用者には知る権利がなかった」の3種類のエラーに対応しなければならないとします。

またシステム内のあらゆるところで使われる可能性があるため、他のエラーに誘起される可能性があるとします。

Step1;「他のエラーに誘起されるか」

課題に書いてあるとおりです。Yesです。

コードは課題1のStep1と同じなため省略します。

Step2;「エラーはどんな種類があるか?」

「蔵書を発見できなかった」、「蔵書を発見できたが利用不可能な状態だった」、「利用者には知る権利がなかった」の3種類ですね。

それぞれ「NotFound」、「NotAvailable」、「PermissionDenied」として次のように列挙型でコードします。

ついでに?のところも、これで埋めます。

use std::error;

#[derive(Clone)]
pub enum ErrorKind {
    NotFound,
    NotAvailable,
    PermissionDenied
}

pub struct Error {
    _error: _Error,
}

impl Error {
    fn new<E>(kind: ErrorKind, error: E) -> Self
        where
            E: Into<Box<dyn error::Error + Send + Sync>>
    {
        Error{ _error: _Error::Custom((kind, error.into())) }
    }
}

enum _Error {
    Simple( ErrorKind ),
    Custom( (ErrorKind, Box<dyn error::Error + Send + Sync>) ),
}

Step2.5;「ErrorKindに説明をつける」

DisplayDebugのメソッドにErrorKindの説明を書き込んでもダメとはいいませんが、、、おすすめはしません。

ErrorKindに説明を返してもらうためのメソッドをつけたほうが後々、書き換える必要が出たときなど楽です。

そこでStep3に行く前にErrorKindにちょっとした実装をしておきます。

impl ErrorKind {
    pub fn description(&self) -> &'static str {
        match self {
            ErrorKind::NotFound => "蔵書が見つかりませんでした。",
            ErrorKind::NotAvailable => "蔵書は利用可能な状態でありません。",
            ErrorKind::PermissionDenied => "あなたのセキュリティクリアランスでは開示されていません。"
        }
    }
}

また自作Errorから中身のErrorKindを取り出すことは多々ありますので

impl Error {
    fn kind(&self) -> ErrorKind {
        match &self._error {
            Simple(s) => s.clone(),
            Custom(c) => c.0.clone(),
        }
    }
}

も実装すると、後々ちょっと楽です。しかし、今は試しに放置しておきます。

Step3;「必要なトレイトを実装していく」

Step2.5でここはかなり簡単になります。実際はDebugDisplayで表示内容を変えたりする必要もあることもありますが、今回は無視して同じの使います。

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self._error {
            _Error::Simple(s) => f.write_str(s.description()),
            _Error::Custom(c) => f.write_str(c.0.description()),
        }
        //
        // kind()があったら
        //     f.write_str(self.kind().description())
        // の1文で済んでいた。
        //
    }
}

impl fmt::Debug for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        <Self as fmt::Display>::fmt(self, f)
    }
}

impl error::Error for Error {
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
        match &self._error {
            _Error::Simple(_) => None,
            _Error::Custom(c) => c.1.source()
        }
    }
}

こんな感じで、必要最低限のトレイトは実装し終えました。

Step4;「他の必要なメソッド等を書く」

課題1と同様に必要な他のメソッドを追加して終わりです。

お疲れ様でした。

5
4
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4