[Rust]自作Errorを作りたくて`std::io::Error`を読んでみた!


Error型自作の実践編を書きました

記事はこちらです。よかったらご覧ください。

Errorを自作したい!

Threadから独自のErrorを含んだResultを返したくなった!

私は今までResult<T,String>で誤魔化してきた。しかし、それはもうやめだ!

0からは、さすがにどうすればいいかわからないので、初めにerror::Errorを読んで見る。

error::Errorを読む

error::Errorトレイトとは、エラーの値を表す基本的な型であることを示しているトレイトです。エラーを表す型は、自分自身を説明できるように、DisplayDebugを実装していなくてはなりません。

このトレイトが持つsourceメソッドは一般的に'アブストラクト・バウンダー'1を超えたときに使われます。もし、あるモジュールが、低レベルのモジュールで発生したエラーによって引き起こされたエラーを報告するとき、sourceメソッドによって、その低レベルのエラーへアクセスすることができます。これによって高レベルのモジュールは独自のErrorを提供しながら、デバックのための実装をsourceのチェーンによって明らかにすることができます。

(以上、error::Errorの俺的意訳)

ざっくりいうと「error::ErrorResult<T,E>のEに用いられるような型であることを示し、このトレイトの持つsourceメソッドを用いるとエラーをさかのぼることができる(さかのぼれるようにしなければならない)。」ということ。

error::Errorの非推奨なメソッド

次の2つのメソッドは非推奨あつかいになっています。

descriptionはエラーの説明を文字列で返すメソッドですが、説明が必要なときはこれを使わず、Displayto_stringメソッドを使うことを推奨しています。

causesourceに置き換えられるという形で非推奨になりました。

error::Errorには4つのメソッドがあり、その内、1つがNightlyで2つが非推奨。よって実際に書くのは1つだけですね!

error::Errorはわかったけど、どう実装すればいいの?

Error自作の肝であるerror::Errorについてはわかりました。しかし、どう実装すればいいのかわかりません。

そのためstd::io::Errorを読んで勉強します。

std::io::Errorの定義

std::io::Errorに関わる重要そうな定義を見ていく。

以下、ここから重要そうな場所の抜粋。

enum Repr {
    Os(i32),
    Simple(ErrorKind),
    Custom(Box<Custom>),
}

struct Custom {
    kind: ErrorKind,
    error: Box<dyn error::Error + Send + Sync>,
}


pub struct Error {
    repr: Repr,
}

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

    fn _new(kind: ErrorKind, error: Box<dyn error::Error + Send + Sync>) -> Error {
        Error { repr: Repr::Custom(Box::new(Custom { kind, error })) }
    }
}

impl Debug for Error { // something }

impl Display for Error {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.repr {
            Repr::Os(code) => {
                let detail = sys::os::error_string(code);
                write!(fmt, "{} (os error {})", detail, code)
            }
            Repr::Custom(ref c) => c.error.fmt(fmt),
            Repr::Simple(kind) => write!(fmt, "{}", kind.as_str()),
        }
    }
}

impl error::Error for Error{
    fn description(&self) -> &str {
        match self.repr {
            Repr::Os(..) | Repr::Simple(..) => self.kind().as_str(),
            Repr::Custom(ref c) => c.error.description(),
        }
    }

    fn cause(&self) -> Option<&dyn error:Error> {
        match self.repr {
            Repr::Os(..) => None
            Repr::Simple(..) => None,
            Repr::Custom(ref c) => c.error.cause(),
        }
    }

    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
        match self.repr {
            Repr::Os(..) => None,
            Repr::Simple(..) => None,
            Repr::Custom(ref c) => c.error.source(),
        }
    }
}

enum ErrorKind{
    NotFound,
    PermissionDenied,
    // etc
}

impl ErrorKind {
    pub(crate) fn as_str(&self) -> &'static str {
        // エラーの種類ごとにmatchで、そのエラーの概要を
        // 表す文字列に変えて返す。
    }
}

impl From<ErrorKind> for Error { 
    fn from(kind: ErrorKind) -> Error {
        Error { repr: Repr::Simple(kind) }
    }
}

std::io::Errorが表す種類

実装を見るとstd::io::Errorは大雑把にOsSimpleCustomの3種類のエラーを表すことがわかります。

詳しく見ていませんが私が想像する

種類 説明
Os OSレベルのエラーを表す
Simple std::ioの関数内で発生したエラーを表す
Custom 上2つ以外の独特なエラーを表す

だと、予想しました。

Osを表すstd::io::Errorの作成

Osを表すstd::io::Errorを作るにはlast_os_errorfrom_raw_os_errorを実行します。

この種のエラーについては、Error自作の基本にかかわらないため、スルーします。2

Simpleを表すstd::io::Errorの作成

Simpleを表すstd::io::ErrorFrom<ErrorKind>が実装されてるので、これを用います。

// 実装
impl From<ErrorKind> for Error { 
    fn from(kind: ErrorKind) -> Error {
        Error { repr: Repr::Simple(kind) }
    }
}

// 例
let error = Error::from(ErrorKind::NotFound);

Customを表すstd::io::Errorの作成

Customを表すstd::io::Errornewから作られます。

//実装
impl Error {
    pub fn new<E>(kind: ErrorKind, error: E) -> Error
    where
        E: Into<Box<dyn error::Error + Send + Sync>>
    {
        Self::_new(kind, error.into())
    }

    fn _new(kind: ErrorKind, error: Box<dyn error::Error + Send + Sync>) -> Error {
        Error { repr: Repr::Custom(Box::new(Custom { kind, error })) }
    }
}

// 例 文字列からエラーを作成する
let custom_error = Error::new(ErrorKind::Other, "oh no!");

// 例 他のエラーからエラーを作成する
let custom_error2 = Error::new(ErrorKind::Interrupted, custom_error);

newを読んでみると、次のようなことが書かれています。

任意のエラー内容と同様の既知の種類のエラーからI/Oエラーを作成します。

この関数は一般的にOSから来たオリジナルではないI/Oエラーを作るために使用されます。errorの引数は、この型に含まれる任意の内容が入れられます。

(以上、newの俺的意訳)

なるほど。つまり、OSからのエラーなどの低レベルから連鎖してきたエラーなら、newを使って作成ということですね。

そしてCustomは、低層からのエラーと自身のいる層のエラー、の両方を含むものだよ。と。

補足:Into<Box<dyn error::Error + Send + Sync>>のブランケット実装

Box<dyn Error + Send + Sync + 'a>他のError&strからFromで作成することができます。(他のError&strはリンクになってます。)

すなわち、他のError&strにはInto<Box<dyn error::Error + Send + Sync>>がブランケット実装されているのです。

これによって例のようなnewができるわけですね。

改めてerror::Errorを考える

ここで、ちょっとstd::io::Errorerror::Error実装部分を読んで、descriptioncausesourceについて復習します。

実装は次の通りです。

impl error::Error for Error{
    fn description(&self) -> &str {
        match self.repr {
            Repr::Os(..) | Repr::Simple(..) => self.kind().as_str(),
            Repr::Custom(ref c) => c.error.description(),
        }
    }

    fn cause(&self) -> Option<&dyn error:Error> {
        match self.repr {
            Repr::Os(..) => None
            Repr::Simple(..) => None,
            Repr::Custom(ref c) => c.error.cause(),
        }
    }

    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
        match self.repr {
            Repr::Os(..) => None,
            Repr::Simple(..) => None,
            Repr::Custom(ref c) => c.error.source(),
        }
    }
}

確かに、causesourceも同じような実装になっています。

またDisplayの実装を見てみると

impl Display for Error {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.repr {
            Repr::Os(code) => {
                let detail = sys::os::error_string(code);
                write!(fmt, "{} (os error {})", detail, code)
            }
            Repr::Custom(ref c) => c.error.fmt(fmt),
            Repr::Simple(kind) => write!(fmt, "{}", kind.as_str()),
        }
    }
}

となっており、descriptionと同じ働きをしているのがわかります。

まとめ

自作Error(error::Errorを実装した自作の型)を作るには

  1. 対応したいエラーが、他のエラーによって誘起されるかどうか考える。

  2. エラーの種類を表す型(ErrorKind等)を作成する。
    -> もし他のエラーから誘起されるのなら誘起元のエラーを格納できるようにする。

  3. 上の2番で作ったものを格納する自作Errorを定義する。

  4. 自作Errorにerror::Errorを実装する。
    -> もし他のエラーに誘起されるのならsourceで誘起元のエラーを返せるようにする。
    -> (sourceにはデフォルトの実装が定義されているため誘起されないなら不要)

  5. DisplayDebugを実装する。
    -> このとき自作Errorの説明が返されるように実装する。

  6. おわり。♪(((^-^)八(^∇^)))♪


  1. ごめんなさい。なんて訳せばいいのかわからなかった。全文読んだら、なんとなく言いたいことはわかると思います。 

  2. もっと言えば、実装自体が難しくないため、説明しなくてもだいたい読めばわかると思います。