コンテンツにスキップ

Rustの独自のエラー型

Resultのページでは、エラーはすべてただの String でした。小さな例ならそれで十分ですが、1つの関数が何通りもの違う理由で失敗しうるようになると、うまくいかなくなります。String は match で分岐できないので、呼び出す側はテキストそのものを解析しない限り、どの種類の失敗が起きたのかを区別できないのです。

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)を使えば解決できます。失敗の種類ごとに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 で細かく分岐できます。

実際のプロジェクトでは、独自のエラー型ごとに Display やその周りの決まりきったコードを手で書くのは、だんだん面倒になってきます。thiserror(英語)クレートを使えば、その大部分を #[derive] で自動生成できます。また anyhow(英語)は、エラーごとに型を定義せず、ただエラーを伝播させたいアプリケーションのコードでよく使われています。こういうものがあると知っておくと便利ですが、基本を学ぶ段階では必要ありません。準備ができたら、anyhowとthiserrorのページで、両方の使い方と、どちらを選べばいいのかを見てみましょう。

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バイトなので、文字数と一致します。

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 のようなクレートで決まりきったコードのほとんどを省けます。

理解度チェック

1. 関数が何通りもの理由で失敗しうるとき、Stringのエラー型では足りないのはなぜですか?

2. withdraw を呼び出す側が、InsufficientFunds と NegativeAmount で違う対応をできるのは何のおかげですか?

3. 独自のエラー型にDisplayを実装すると、何ができるようになりますか?

4. thiserror や anyhow のようなクレートは何に役立ちますか?

5. エラーメッセージの文字列をそのまま比べるのが「もろい」と言われるのはなぜですか?

スコア: 0 / 5