項目4:標準のError型を使おう
https://effective-rust.com/errors.html
LT;DR
std::error::Error トレイトの実装は容易なので、エラー型を定義するなら実装すべき
元になるエラーが複数種類ある場合、それらの型を保持する必要があるかは考えるべき
必要な場合: アプリケーションコードでは anyhow を使ってラップする
不要な場合: enum で表現し、thiserror を用いて簡単に変換を定義する
いずれにせよ、型システムを用いて宣言しよう
項目1:データ構造を表現するために型システムを用いよう
hr.icon
Error トレイト
https://doc.rust-lang.org/std/error/trait.Error.html
エラーの共通 インタフェース となる標準 トレイト
トレイトを実装するには、以下の 2 つのトレイトを実装する必要がある
Display: format! で {} を用いるため
Debug: format! で {:?} を用いるため
メソッドは source のみを持つ(deprecated になっているものは除く)
code:rs
fn source(&self) -> Option<&(dyn Error + 'static)>
Error の内部に保持するエラーを開示するために用いる
None を返す デフォルト実装 がされている
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 な環境でも利用可能
項目33:ライブラリコードをno_std互換にすることを検討しよう
最小限のエラー
情報を保持する必要がない場合 String で構わない
数少ない Stringly typed な変数が適切なケース
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 {}
✅ Newtype パターン を利用する
String 型をラップするタプル構造を作れば、Error トレイトは実装可能
ただし、Debug と Display も実装する必要がある
code:rs
#derive(Debug)
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
#derive(Debug)
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)
}
ここまでを踏まえると、安全なエラー型を定義するには、かなりの量の典型的なコードを書く必要がある
この問題は derive マクロを使うことで回避できるが、車輪の再発明 となるため thiserror クレートを利用 するのが良い
トレイトオブジェクト
項目4:標準のError型を使おう#6761776975d04f0000aa55af の方法では、エラーの詳細をすべて捨てて文字列情報(format!("{:?}", e))だけを保持していた
項目4:標準のError型を使おう#67617b6d75d04f0000a2a0a3 の方法では、可能なすべてのエラー情報を保持できるが、可能性のあるすべてのエラーを列挙する必要がある
上記を解決するには トレイトオブジェクト を用いると良いように思える
しかし、すべてのエラーを書き出す必要はないが 型に関する情報は消える
そのため、source や Display::fmt、Debug::fmt など、Error トレイトとそのトレイト境界が提供するメソッドにはアクセスできるが、元のエラーが持つメソッドはアクセスできない
code:rs
#derive(Debug)
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),
}
}
}
また、Rust の トレイトオブジェクト安全性 や Coherence rule(一貫性ルール)があるため、Error と From トレイトの両方を実装するのが面倒である
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 を生成する ブランケット実装(reflextive implementation)との 衝突(ある型に対して適用されるトレイト実装を 1 つに絞り込めない)が起きる
標準ライブラリで定義されている 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
ただし、ライブラリは例外
https://nick.groenen.me/posts/rust-error-handling/
ライブラリの場合
コードは使われる環境を予想できないため、具体的で詳細なエラー情報を出力して利用者がその情報を利用できるようにする必要がある
この場合、enum を用いたエラーを利用するのが良い
また、ライブラリの API で anyhow に依存するのは避けたほうが良い
項目24:APIに型が登場する依存ライブラリは再エクスポートしよう
アプリケーションの場合
コードはエラーを エンドユーザ に見せる方法に注力すべきである
e.g. 人間が読めるメッセージや適切なログ形式で表示するかなど
また、依存グラフ にあるすべてのライブラリが吐く様々なエラーに対応する必要がある
項目25:依存グラフを管理しよう
そのため、anyhow::Error などを用いて、より動的なエラー処理を行ったほうがエラー処理が単純になる
また、アプリケーション全体で一貫したものになる
#Rust #Effective_Rust_―_Rustコードを改善し、エコシステムを最大限に活用するための35項目