エラーハンドリング応用
実践的なエラー処理のパターンを学びます。
? 演算子
エラーを呼び出し元に伝播させます。
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};
// ? を使わない場合
fn read_file_verbose(path: &str) -> Result<String, io::Error> {
let file = match File::open(path) {
Ok(f) => f,
Err(e) => return Err(e),
};
let mut contents = String::new();
match file.read_to_string(&mut contents) {
Ok(_) => Ok(contents),
Err(e) => Err(e),
}
}
// ? を使う場合(簡潔!)
fn read_file(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
}
? の動作
#![allow(unused)]
fn main() {
let value = some_result?;
// は以下と同等
let value = match some_result {
Ok(v) => v,
Err(e) => return Err(e.into()),
};
}
チェーン
#![allow(unused)]
fn main() {
fn read_file(path: &str) -> Result<String, io::Error> {
let mut contents = String::new();
File::open(path)?.read_to_string(&mut contents)?;
Ok(contents)
}
// さらに簡潔に
fn read_file_short(path: &str) -> Result<String, io::Error> {
std::fs::read_to_string(path)
}
}
カスタムエラー型
基本的なカスタムエラー
#![allow(unused)]
fn main() {
#[derive(Debug)]
enum AppError {
NotFound(String),
InvalidInput(String),
IoError(std::io::Error),
}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
AppError::NotFound(msg) => write!(f, "Not found: {}", msg),
AppError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
AppError::IoError(e) => write!(f, "IO error: {}", e),
}
}
}
impl std::error::Error for AppError {}
}
From トレイトで変換を自動化
#![allow(unused)]
fn main() {
impl From<std::io::Error> for AppError {
fn from(error: std::io::Error) -> Self {
AppError::IoError(error)
}
}
// これで ? が自動変換してくれる
fn do_something() -> Result<(), AppError> {
let _file = std::fs::File::open("file.txt")?; // io::Error -> AppError
Ok(())
}
}
thiserror クレート(推奨)
カスタムエラーを簡単に定義できます。
Cargo.toml
[dependencies]
thiserror = "1.0"
#![allow(unused)]
fn main() {
use thiserror::Error;
#[derive(Error, Debug)]
enum AppError {
#[error("User not found: {0}")]
NotFound(String),
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Database error")]
DatabaseError(#[from] sqlx::Error),
#[error("IO error")]
IoError(#[from] std::io::Error),
}
}
anyhow クレート(アプリケーション向け)
エラー型を気にせず簡単にエラー処理できます。
Cargo.toml
[dependencies]
anyhow = "1.0"
use anyhow::{Result, Context};
fn read_config() -> Result<String> {
let content = std::fs::read_to_string("config.toml")
.context("設定ファイルの読み込みに失敗")?;
Ok(content)
}
fn main() -> Result<()> {
let config = read_config()?;
println!("{}", config);
Ok(())
}
エラー処理のパターン
パターン1: 早期リターン
#![allow(unused)]
fn main() {
fn process_user(id: u32) -> Result<User, AppError> {
if id == 0 {
return Err(AppError::InvalidInput("ID cannot be 0".into()));
}
let user = find_user(id)?;
if !user.is_active {
return Err(AppError::NotFound("User is inactive".into()));
}
Ok(user)
}
}
パターン2: map_err で変換
#![allow(unused)]
fn main() {
fn read_config() -> Result<Config, AppError> {
let content = std::fs::read_to_string("config.toml")
.map_err(|e| AppError::IoError(e))?;
toml::from_str(&content)
.map_err(|e| AppError::InvalidInput(e.to_string()))
}
}
パターン3: ok_or / ok_or_else
#![allow(unused)]
fn main() {
fn get_user(id: u32) -> Result<User, AppError> {
users.get(&id)
.cloned()
.ok_or_else(|| AppError::NotFound(format!("User {} not found", id)))
}
}
パターン4: and_then でチェーン
#![allow(unused)]
fn main() {
fn process() -> Result<String, AppError> {
read_file("input.txt")
.and_then(|content| parse_content(&content))
.and_then(|data| transform_data(data))
.and_then(|result| save_result(result))
}
}
main関数でのエラー処理
use std::process;
fn main() {
if let Err(e) = run() {
eprintln!("エラー: {}", e);
process::exit(1);
}
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
let config = read_config()?;
let data = process_data(&config)?;
save_output(&data)?;
Ok(())
}
パニックとの使い分け
| 状況 | 使うもの |
|---|---|
| 回復可能なエラー | Result<T, E> |
| プログラムのバグ | panic! |
| テスト | unwrap(), expect() |
| プロトタイプ | unwrap() |
| 本番コード | 適切なエラー処理 |
#![allow(unused)]
fn main() {
// panic! を使う場面(プログラムのバグ)
fn get_element(index: usize) -> &str {
let items = ["a", "b", "c"];
items.get(index).expect("Index should be valid")
}
// Result を使う場面(回復可能なエラー)
fn find_user(id: u32) -> Result<User, AppError> {
// ...
}
}
まとめ
| 概念 | 説明 |
|---|---|
? | エラーを伝播 |
| カスタムエラー | アプリ固有のエラー型 |
thiserror | エラー型定義を簡単に |
anyhow | 柔軟なエラー処理 |
map_err | エラー型を変換 |
context | エラーに文脈を追加 |
確認テスト
Q1. ?演算子の動作は?
正解: B)
?はResultがErrの場合、そのエラーを関数からreturnします。Okの場合は中の値を取り出します。
Q2. thiserrorとanyhowの使い分けは?
正解: A)
thiserrorは具体的なエラー型を定義するのに適しており、ライブラリで使います。anyhowは様々なエラーを簡単に扱えるため、アプリケーションで使います。
Q3. map_errの役割は?
正解: C)
map_errはResultのErrの値を別の型に変換します。異なるエラー型を統一するのに便利です。
Q4. 以下のコードを?で簡潔にした結果は?let content = match std::fs::read_to_string(path) { Ok(c) => c, Err(e) => return Err(e) };
正解: B)
?演算子はOkの場合は値を取り出し、Errの場合は早期リターンします。まさにmatchで書いていた処理と同等です。
Q5. panic!を使うべき場面は?
正解: D)
panic!はプログラムのバグなど回復不能な状態で使います。ファイルエラーやネットワークエラー、ユーザー入力エラーはResultで適切に処理すべきです。
Phase 3 完了!
おめでとうございます!Phase 3を完了しました。
学んだこと:
- 構造体(struct、impl)
- 列挙型とパターンマッチング
- トレイト
- ジェネリクス
- モジュールシステム
- エラーハンドリング応用
次のPhase: Phase 4: エコシステムと実践