項目4:標準のError型を使おう
LT;DR
std::error::Error トレイトの実装は容易なので、エラー型を定義するなら実装すべき
元になるエラーが複数種類ある場合、それらの型を保持する必要があるかは考えるべき
必要な場合: アプリケーションコードでは anyhow を使ってラップする いずれにせよ、型システムを用いて宣言しよう
hr.icon
Error トレイト
トレイトを実装するには、以下の 2 つのトレイトを実装する必要がある
Display: format! で {} を用いるため
Debug: format! で {:?} を用いるため
メソッドは source のみを持つ(deprecated になっているものは除く)
code:rs
fn source(&self) -> Option<&(dyn Error + 'static)>
Error の内部に保持するエラーを開示するために用いる
code:rs
fn source(&self) -> Option<&(dyn Error + 'static)> {
None
}
そのため、実装が容易である
warning.icon
Result<T,E> の E は Error トレイトを実装している必要はないが、ラッパが適切な トレイト境界 を表現できるようにするのが良い エラー型は Error を実装しよう
v1.80 までは std に実装されていたが、1.81 以降では core に移されたため、no_std な環境でも利用可能
最小限のエラー
情報を保持する必要がない場合 String で構わない
code:rs
pub fn find_user(_username: &str) -> Result<UserId, String> {
let _f = std::fs::File::open("/etc/passwd")
.map_err(|e| format!("failed to open /etc/passwd: {:?}", e))?;
// ...
}
しかし、String は Error トレイトを実装していない
どう実装できるか?
🔴 String に対して impl Error する
トレイトも型も自分で定義したものでない場合は実装できない(孤児ルール)ため code:rs
impl std::error::Error for String {}
新しい型を作り出すわけではないので、同じメッセージが表示される
code:rs
pub type MyError = String;
impl std::error::Error for String {}
String 型をラップするタプル構造を作れば、Error トレイトは実装可能
ただし、Debug と Display も実装する必要がある
code:rs
pub struct MyError(String);
impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for MyError {}
From<String> トレイトを実装しておくと、文字列から MyError インスタンスへの変換が容易になる
code:rs
impl From<String> for MyError {
fn from(value: String) -> Self {
Self(value)
}
}
これにより、? を用いると、コンパイラが自動的に目的のエラー型に変換する From トレイトの実装を適用 するようになる
ネストしたエラー
重要なエラーの詳細を保持しておき、呼び出し元に伝えるには、enum を用いると良い
code:rs
pub enum MyError {
Io(std::io::Error),
Utf8(std::string::FromUtf8Error),
General(String),
}
impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(e) => write!(f, "IO error: {}", e),
Self::Utf8(e) => write!(f, "UTF-8 error: {}", e),
Self::General(s) => write!(f, "General error: {}", s),
}
}
}
このとき、内部のエラーに簡単にアクセスできるように、source を オーバーライド するのが良い code:rs
impl std::error::Error for MyError {
fn cause(&self) -> Option<&dyn std::error::Error> {
match self {
Self::Io(e) => Some(e),
Self::Utf8(e) => Some(e),
Self::General(_) => None,
}
}
}
また、From トレイトも実装しておくと良い
code:rs
impl From<std::io::Error> for MyError {
fn from(value: std::io::Error) -> Self {
Self::Io(value)
}
}
impl From<std::string::FromUtf8Error> for MyError {
fn from(value: std::string::FromUtf8Error) -> Self {
Self::Utf8(value)
}
}
これにより、ライブラリの利用者が orphans rule に悩まされることがなくなる
また、? が自動的に必要な From 変換を行うため、map_err が不要になる
code:rs
pub fn first_line(filename: &str) -> Result<String, MyError> {
let file = std::fs::File::open(filename).map_err(MyError::Io)?;
let mut reader = std::io::BufReader::new(file);
let mut buf = vec![];
let len = reader.read_until(b'\n', &mut buf)?;
let result = String::from_utf8(buf)?;
if result.len() > MAX_LEN {
return Err(MyError::General(format!("Line too long: {}", len)));
}
Ok(result)
}
ここまでを踏まえると、安全なエラー型を定義するには、かなりの量の典型的なコードを書く必要がある
トレイトオブジェクト
しかし、すべてのエラーを書き出す必要はないが 型に関する情報は消える
そのため、source や Display::fmt、Debug::fmt など、Error トレイトとそのトレイト境界が提供するメソッドにはアクセスできるが、元のエラーが持つメソッドはアクセスできない
code:rs
pub enum WrappedError {
Wrapped(Box<dyn Error>),
General(String)
}
impl std::fmt::Display for WrappedError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Wrapped(e) => write!(f, "Inner error: {}", e),
Self::General(s) => write!(f, "{}", s),
}
}
}
Coherence rule: ある型に対するトレイトの実装は 1 つだけ
たとえば、以下はコンパイルエラーとなる
code:rs
impl Error for WrappedError {}
impl<E: 'static + Error> From<E> for WrappedError {
fn from(e: E) -> Self {
Self::Wrapped(Box::new(e))
}
}
標準ライブラリで定義されている T から T を生成するブランケット実装
code:rs
impl<T> From<T> for T {
fn from(item: T) -> T {
item
}
}
radish-miyazaki.icon
より具体的な例
code:rs
let error: WrappedError = WrappedError::General("hoge".into());
let wrapped: WrappedError = error.into();
上記のコードの場合、コンパイラは以下の 2 つの候補を検討する
1. 標準ライブラリの impl<T> From<T> for T
T = WrappedError なので、WrappedError をそのまま WrappedError として変換する
2. 上記の実装
E = WrappedError と解釈される可能性がある
anyhow では、上記の問題を Box を介した間接参照レベルを追加することで解決している 「アプリケーションのエラー処理には anyhow の利用を検討しよう」
warning.icon
ただし、ライブラリは例外
ライブラリの場合
コードは使われる環境を予想できないため、具体的で詳細なエラー情報を出力して利用者がその情報を利用できるようにする必要がある
この場合、enum を用いたエラーを利用するのが良い
また、ライブラリの API で anyhow に依存するのは避けたほうが良い
アプリケーションの場合
コードはエラーを エンドユーザ に見せる方法に注力すべきである e.g. 人間が読めるメッセージや適切なログ形式で表示するかなど
また、依存グラフ にあるすべてのライブラリが吐く様々なエラーに対応する必要がある そのため、anyhow::Error などを用いて、より動的なエラー処理を行ったほうがエラー処理が単純になる
また、アプリケーション全体で一貫したものになる