Rustの独自のエラー型
Resultのページでは、エラーはすべてただの String でした。小さな例ならそれで十分ですが、1つの関数が何通りもの違う理由で失敗しうるようになると、うまくいかなくなります。String は match で分岐できないので、呼び出す側はテキストそのものを解析しない限り、どの種類の失敗が起きたのかを区別できないのです。
Stringのエラーの問題点
Section titled “Stringのエラーの問題点”fn withdraw(balance: f64, amount: f64) -> Result<f64, String> { if amount > balance { return Err(String::from("残高が足りません")); } if amount < 0.0 { return Err(String::from("金額をマイナスにはできません")); } Ok(balance - amount)}呼び出す側が「残高不足」と「マイナスの金額」で違う対応をしたい場合、エラーの文字列そのものを比べるしかありません。これはもろいやり方で、メッセージの言い回しを少し変えただけで壊れてしまいます。
エラーをenumとして定義する
Section titled “エラーをenumとして定義する”列挙型(enum)を使えば解決できます。失敗の種類ごとに1つのバリアントを用意すれば、呼び出す側はそれを直接 match できます。
enum WithdrawError { InsufficientFunds, NegativeAmount,}
fn withdraw(balance: f64, amount: f64) -> Result<f64, WithdrawError> { if amount < 0.0 { return Err(WithdrawError::NegativeAmount); } if amount > balance { return Err(WithdrawError::InsufficientFunds); } Ok(balance - amount)}
fn main() { match withdraw(100.0, 150.0) { Ok(remaining) => println!("新しい残高: {remaining}"), Err(WithdrawError::InsufficientFunds) => println!("お金が足りません"), Err(WithdrawError::NegativeAmount) => println!("マイナスの金額は引き出せません"), }}実行結果:
お金が足りませんこれで、すべての種類の失敗が処理されていることをコンパイラがチェックしてくれます。enumに対して match がいつも保証してくれていたことを、そのままエラーにも当てはめただけです。
Displayを実装してきちんとしたエラーメッセージにする
Section titled “Displayを実装してきちんとしたエラーメッセージにする”今のままで WithdrawError を {} で表示しようとすると、コンパイルできません。Rustはこれをテキストに変える方法を知らないからです。Display トレイトを実装すれば解決します。トレイトについては、このあと専用のページ(トレイトとよく使うトレイト)で詳しく学びます。今のところは、次のパターンをそのまま真似すれば大丈夫です。
use std::fmt;
enum WithdrawError { InsufficientFunds, NegativeAmount,}
impl fmt::Display for WithdrawError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { WithdrawError::InsufficientFunds => write!(f, "残高が足りません"), WithdrawError::NegativeAmount => write!(f, "金額をマイナスにはできません"), } }}
fn main() { let error = WithdrawError::InsufficientFunds; println!("エラー: {error}");}実行結果:
エラー: 残高が足りませんこれで WithdrawError は、ユーザーやログに表示したいほかのエラーと同じようにふるまいます。それでいて、特定のケースに反応したいコードからは、今までどおり match で細かく分岐できます。
クレートの力を借りる
Section titled “クレートの力を借りる”実際のプロジェクトでは、独自のエラー型ごとに Display やその周りの決まりきったコードを手で書くのは、だんだん面倒になってきます。thiserror(英語)クレートを使えば、その大部分を #[derive] で自動生成できます。また anyhow(英語)は、エラーごとに型を定義せず、ただエラーを伝播させたいアプリケーションのコードでよく使われています。こういうものがあると知っておくと便利ですが、基本を学ぶ段階では必要ありません。準備ができたら、anyhowとthiserrorのページで、両方の使い方と、どちらを選べばいいのかを見てみましょう。
1. ログインのエラー
Section titled “1. ログインのエラー”EmptyUsername と PasswordTooShort の2つのバリアントを持つ列挙型 LoginError を定義し、Display を実装してください。そのうえで、Ok でようこそメッセージを返すか、適切なエラーを返す fn check_login(user: &str, password: &str) -> Result<String, LoginError> を書きます。パスワードは8文字以上でなければいけません。ヒント:&str の .is_empty() と .len() を使えば、必要なチェックができます。
解答を見る
use std::fmt;
enum LoginError { EmptyUsername, PasswordTooShort,}
impl fmt::Display for LoginError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { LoginError::EmptyUsername => write!(f, "ユーザー名を空にすることはできません"), LoginError::PasswordTooShort => write!(f, "パスワードは8文字以上にしてください"), } }}
fn check_login(user: &str, password: &str) -> Result<String, LoginError> { if user.is_empty() { return Err(LoginError::EmptyUsername); } if password.len() < 8 { return Err(LoginError::PasswordTooShort); } Ok(format!("ようこそ、{user}さん!"))}
fn main() { let attempts = [("ana", "correct-horse"), ("", "secret123"), ("ben", "short")];
for (user, password) in attempts { match check_login(user, password) { Ok(message) => println!("{message}"), Err(e) => println!("エラー: {e}"), } }}実行結果:
ようこそ、anaさん!エラー: ユーザー名を空にすることはできませんエラー: パスワードは8文字以上にしてください.len() が数えるのはバイト数なので、パスワードの例は英数字にしています。英数字なら1文字が1バイトなので、文字数と一致します。
2. 詳しい情報を持つエラー
Section titled “2. 詳しい情報を持つエラー”fn parse_percent(input: &str) -> Result<u32, PercentError> を書いてください。PercentError には、データを持つ2つのバリアント、NotANumber(String)(変換できなかったテキスト)と TooLarge(u32)(100を超えた数)を用意します。Display のメッセージには、それらの詳しい情報を含めましょう。ヒント:列挙型のページで見たように、列挙型のバリアントは値を持てます。fmt の中では、PercentError::TooLarge(n) => ... のように match して値を取り出します。
解答を見る
use std::fmt;
enum PercentError { NotANumber(String), TooLarge(u32),}
impl fmt::Display for PercentError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { PercentError::NotANumber(text) => write!(f, "'{text}' は数値ではありません"), PercentError::TooLarge(n) => write!(f, "{n} は100を超えています"), } }}
fn parse_percent(input: &str) -> Result<u32, PercentError> { match input.parse::<u32>() { Ok(n) => { if n > 100 { Err(PercentError::TooLarge(n)) } else { Ok(n) } } Err(_) => Err(PercentError::NotANumber(input.to_string())), }}
fn main() { for input in ["75", "半分", "250"] { match parse_percent(input) { Ok(n) => println!("{n}%"), Err(e) => println!("エラー: {e}"), } }}実行結果:
75%エラー: '半分' は数値ではありませんエラー: 250 は100を超えていますStringのエラーは小さな例なら使えますが、呼び出す側が種類ごとにmatchで区別することはできません。- 失敗の種類ごとに1つのバリアントを持つenumを定義すれば、呼び出す側は特定のエラーを
matchで扱えます。 Displayを実装する(impl fmt::Display for YourError)と、そのエラー型を{}で表示できるようになります。- 大きなプロジェクトでは、
thiserrorやanyhowのようなクレートで決まりきったコードのほとんどを省けます。
理解度チェック
Section titled “理解度チェック”理解度チェック
1. 関数が何通りもの理由で失敗しうるとき、Stringのエラー型では足りないのはなぜですか?
2. withdraw を呼び出す側が、InsufficientFunds と NegativeAmount で違う対応をできるのは何のおかげですか?
3. 独自のエラー型にDisplayを実装すると、何ができるようになりますか?
4. thiserror や anyhow のようなクレートは何に役立ちますか?
5. エラーメッセージの文字列をそのまま比べるのが「もろい」と言われるのはなぜですか?
スコア: 0 / 5
さらに詳しく
Section titled “さらに詳しく”- エラー型を定義する — Rust By Example 日本語版:
Displayを実装した独自のエラー型の、短くて実行できる例です。 - エラーをラップする — Rust By Example 日本語版:次のステップです。ほかのエラーを包み込み、
Fromを通して?と組み合わせて使えるエラーの列挙型を紹介しています。 std::error::Error— 標準ライブラリのドキュメント(英語):「本格的な」エラー型が実装する標準のトレイトです。source()メソッドが何のためにあるのかもわかります。- panic!すべきかするまいか — The Rust Programming Language 日本語版:エラーを返すべきときと、プログラムを止めるべきときの指針です。
- thiserror — docs.rs(英語):エラーの列挙型に
DisplayとErrorを derive してくれるクレートです。書くコードがぐっと減ります。