Rustのエラー処理:`Result`と`?`演算子
エラー処理は、Rustの最も注目すべき設計上の特徴の一つです。例外に依存せず、代わりに型システムを用いて「潜在的な失敗」を戻り値の型にエンコードし、コンパイラがエラーを見落とすことのないよう保証します。
Rustにはtry-catchやthrowはありません。その代わりに、Result<T, E>列挙型と?演算子が使用されます。これらにより、エラー処理と通常のビジネスロジックが分離され、安全かつ簡潔なコードが実現されます。
1. テイクアウトの注文から受け取りまでのトラブル対応の体験談
(1) 実際のフードデリバリーのプロセス
シャオ・ミンは今日残業をしているため、テイクアウトを注文することにした。その過程には、問題が発生する可能性のある点がいくつかある:
| 手順 | 通常の処理 | 発生しうるエラー |
|---|---|---|
| 1. レストランを探す | フードデリバリーアプリを開いてレストランを検索する | レストランが見つからない |
| 2. メニューの読み込み | メニュー項目の閲覧 | メニューの読み込みに失敗しました(ネットワークタイムアウト) |
| 3. 支払いを送信 | 支払う | 支払いに失敗しました(残高不足) |
| 4. 準備待ち | 30分待つ | 店舗が注文をキャンセル |
| 5. 受け取り | テイクアウトの受け取り | 受け取り時に注文がキャンセルされていたことに気づく |
各ステップは成功(Ok)するか、失敗(Err)する可能性があります。これをRustの手法を用いてモデル化してみましょう:
// ============================================
// Use Result to simulate every step of ordering takeout
// ============================================
// Defining Possible Errors
#[derive(Debug)]
enum OrderError {
RestaurantNotFound,
MenuLoadFailed,
PaymentFailed(String),
OrderCancelled,
}
// Simulate Store Search
fn find_restaurant(name: &str) -> Result<String, OrderError> {
let available = vec!["PizzaHouse", "SushiBar", "NoodleShop"];
if available.contains(&name) {
Ok(format!("Found: {}", name))
} else {
Err(OrderError::RestaurantNotFound)
}
}
// Simulate the Load Menu
fn load_menu(restaurant: &str) -> Result<Vec<&str>, OrderError> {
if restaurant.contains("Pizza") {
Ok(vec!["Margherita", "Pepperoni", "Hawaiian"])
} else {
Err(OrderError::MenuLoadFailed)
}
}
// Simulated Payment
fn process_payment(amount: f64) -> Result<String, OrderError> {
if amount < 100.0 {
Ok(format!("Paid: ${:.2}", amount))
} else {
Err(OrderError::PaymentFailed("Insufficient balance".into()))
}
}
fn main() {
// Go through the entire process from start to finish
let restaurant = find_restaurant("PizzaHouse");
match restaurant {
Ok(msg) => println!("Step 1: {}", msg),
Err(e) => println!("Step 1 failed: {:?}", e),
}
}
各ステップにおいて、戻り値が
Result<T, E>—Ok(T)の場合は成功、Err(E)の場合は失敗を示します。呼び出し側は、これら両方の可能性を明示的に処理しなければなりません。「エラー処理を忘れる」といったことは許されません。
2. 概念図
以下のMermaidフローチャートは、Result<T, E>エラーを処理するための4つの主な経路、すなわちmatchによる詳細な処理、?による演算子の伝播、unwrap/expectによる高速評価、およびパニックを示しています:
graph TB
A["Result<T, E>"] --> B["Ok(T)<br/>Success"]
A --> C["Err(E)<br/>Failure"]
B --> D["Continue execution<br/>Standard Procedure"]
C --> E["match Processing<br/>Fine-Grained Branch Control"]
C --> F["? Operators<br/>Propagation Error"]
C --> G["unwrap / expect<br/>Quick Value Retrieval"]
E --> H["For different errors<br/>Handle them separately"]
F --> I["Caller Function<br/>Receive Err"]
G --> J["panic!<br/>Program Crash"]
H --> K["Restore Default Values<br/>Or retry logic"]
I --> L["match in main<br/>Centralized Processing"]
style A fill:#e1f5fe,stroke:#0288d1
style B fill:#c8e6c9,stroke:#388e3c
style C fill:#ffcdd2,stroke:#d32f2f
style J fill:#ffcdd2,stroke:#d32f2f
3. 学習内容
Result<T, E>Enumeration: Rustにおけるエラー処理の中核となる型で、Ok(T)とErr(E)の2つのバリエーションがあるunwrap/expect:値を迅速に取得する方法とそのリスク?Operator: 関数間でエラーを伝播するための簡潔な構文matchエラー処理:さまざまなエラーに対するきめ細かな処理- カスタムエラータイプ:エラーメッセージをより明確にするために、
DisplayおよびDebugを実装する panic!対 エラー戻り値の選択戦略:どのようなシナリオでどのアプローチを採用すべきか
4. 基本概念
graph TB
A[Error Handling Strategies] --> B[Recoverable errors<br>Recoverable]
A --> C[Unrecoverable error<br>Unrecoverable]
B --> D["Result<T, E>"]
D --> E["Ok(T) Success Score"]
D --> F["Err(E) Error value"]
F --> G["match Precision Processing"]
F --> H["? Top-down communication"]
F --> I["unwrap/expect Quick Value Retrieval<br>(There are risks)"]
C --> J["panic!"]
J --> K["The program crashed and exited"]
J --> L["Applicable:Bug/Irreversible state"]
B -.-> M["Custom Error Types"]
M --> N["Implementation Display + Debug"]
M --> O["From trait Convert"]
(1) 4つのエラー処理戦略の比較
| 戦略 | ユースケース | メリット | デメリット |
|---|---|---|---|
panic! |
回復不可能なエラー(配列の範囲外アクセスやアサーション違反など) | 問題を早期に特定するための迅速な失敗 | プログラムが直ちにクラッシュする |
unwrap / expect |
プロトタイプ開発 / 失敗しないことを保証 | 簡潔なコード | エラーが発生すると即座にパニック状態になる――あまり洗練されていない |
match / if let |
エラーの種類ごとに異なる処理が必要 | エラー処理ロジックをきめ細かく制御可能 | 冗長なコード |
? 演算子 |
関数間でエラーを伝播させる;最上位レベルで統一的に処理される | 最も簡潔で、メインのロジックを明確に保つ | Result を返す関数で使用する必要がある |
(2) Result<T, E> クイックリファレンスガイド
| メソッド | シグネチャ | 目的 | 失敗時の挙動 |
|---|---|---|---|
unwrap() |
Result<T,E> -> T |
「OK」から値を抽出 | panic! |
expect(msg) |
Result<T,E> -> T |
「Ok」から値を取得し、パニックメッセージをカスタマイズする | panic!(msg) |
unwrap_or(default) |
Result<T,E> -> T |
成功した場合は値を返し、失敗した場合はデフォルト値を返す | デフォルト値を返す |
unwrap_or_else(fn) |
Result<T,E> -> T |
成功した場合は値を返し、失敗した場合はクロージャを実行する | クロージャを実行する |
is_ok() |
Result<T,E> -> bool |
成功したか確認 | — |
is_err() |
Result<T,E> -> bool |
故障の有無を確認 | — |
ok() |
Result<T,E> -> Option<T> |
Option に変換 |
None |
err() |
Result<T,E> -> Option<E> |
Option へ |
None |
map(fn) |
Result<T,E> -> Result<U,E> |
変換成功値 | 変更なし |
map_err(fn) |
Result<T,E> -> Result<T,F> |
誤った値の変換 | エラータイプの変換 |
and_then(fn) |
Result<T,E> -> Result<U,E> |
連鎖呼び出しにおける後続の演算 | 短絡評価 |
5. 例
(1) ▶ サンプル:基本的な「Result」と「match」の扱い(難易度 ⭐)
// ============================================
// Use Result to handle division-by-zero errors
// ============================================
fn safe_divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err("Division by zero".to_string())
} else {
Ok(a / b)
}
}
fn main() {
// Use match to handle both success and failure scenarios
println!("=== match Processing ===");
match safe_divide(10.0, 2.0) {
Ok(result) => println!("10 / 2 = {}", result),
Err(msg) => println!("Error: {}", msg),
}
match safe_divide(10.0, 0.0) {
Ok(result) => println!("10 / 0 = {}", result),
Err(msg) => println!("Error: {}", msg),
}
// Use unwrap_or to provide a default value
println!("\n=== unwrap_or Default value ===");
let result1 = safe_divide(10.0, 2.0).unwrap_or(0.0);
let result2 = safe_divide(10.0, 0.0).unwrap_or(0.0);
println!("10 / 2 = {}", result1);
println!("10 / 0 = {} (default)", result2);
// Use unwrap_or_else to execute a closure
println!("\n=== unwrap_or_else Closure ===");
let result3 = safe_divide(10.0, 2.0).unwrap_or_else(|e| {
eprintln!("Warning: {}, using default", e);
0.0
});
println!("Result: {}", result3);
// ⚠️ unwrap will panic (Uncomment the code below to try it out)
// let crash = safe_divide(10.0, 0.0).unwrap();
// println!("Will not reach here");
}
出力:
=== match Processing ===
10 / 2 = 5
Error: Division by zero
=== unwrap_or Default value ===
10 / 2 = 5
10 / 0 = 0 (default)
=== unwrap_or_else Closure ===
Result: 5
matchは最も包括的な処理を提供します。つまり、OkとErrに対して個別の処理ロジックを記述することができます。unwrap_orとunwrap_or_elseはショートカットであり、失敗した場合のデフォルト値を提供します。unwrap()は最もリスクが高い方法です。これは成功が保証されていると仮定しており、失敗した場合は即座にクラッシュします。
(2) ▶ サンプル:? 演算子—連鎖的な誤差の伝播 (難易度 ⭐⭐)
// ============================================
// ? Operators: Error propagation with Result
// Only when returning Result can only be used within the function ?
// ============================================
use std::fs::File;
use std::io::{self, Read};
// Read the entire contents of the file
// ? indicates:If File::open Failure,Return Now Err
// If read_to_string Failure,Return Now Err
fn read_file(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?; // Open the file,If it fails, return
let mut content = String::new();
file.read_to_string(&mut content)?; // Read the content,If it fails, return
Ok(content)
}
// Do not use ? An equivalent way of writing——The amount of code has doubled
fn read_file_without_question(path: &str) -> Result<String, io::Error> {
let mut file = match File::open(path) {
Ok(f) => f,
Err(e) => return Err(e),
};
let mut content = String::new();
match file.read_to_string(&mut content) {
Ok(_) => Ok(content),
Err(e) => Err(e),
}
}
// Chain Call ?——More concise
fn read_file_chain(path: &str) -> Result<String, io::Error> {
let mut content = String::new();
File::open(path)?.read_to_string(&mut content)?;
Ok(content)
}
fn main() {
// Test files that can and cannot be read, respectively
let files = vec!["Cargo.toml", "nonexistent.txt"];
for file in &files {
match read_file(file) {
Ok(content) => {
println!("--- {} ({} bytes) ---", file, content.len());
println!("{}", &content[..content.len().min(80)]);
}
Err(e) => {
println!("Failed to read '{}': {}", file, e);
}
}
}
}
出力(Cargo.toml が存在し、かつ空でない場合):
--- Cargo.toml (42 bytes) ---
[package]
name = "demo"
version = "0.1.0"
edition = "2021"
Failed to read 'nonexistent.txt': The system cannot find the file specified. (os error 2)
?演算子は、Rustにおけるエラー処理の要です。これは「失敗した場合は早期に返す」という処理の省略形として機能します。expr?はmatch expr { Ok(v) => v, Err(e) => return Err(e.into()) }と同等です。なお、?は自動的にFrom::fromを呼び出してエラー型の変換を行います。これが、異なるエラー型間でエラーを伝播させるための鍵となります。
(3) ▶ サンプル:カスタムエラー型 — 表示とデバッグの実装 (難易度 ⭐⭐)
// ============================================
// Custom Error Types:Make Your Error Messages More Informative
// To be implemented std::fmt::Display + std::fmt::Debug
// ============================================
use std::fmt;
use std::num::ParseIntError;
// Custom Error Enumeration
#[derive(Debug)]
enum AppError {
/// Input is empty
EmptyInput,
/// Failed to parse the number,Includes the original string
ParseFailed(String),
/// The value is outside the allowed range
OutOfRange { value: i32, min: i32, max: i32 },
/// Division by Zero
DivisionByZero,
}
// Implementation Display——Control the error messages users see
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::EmptyInput => {
write!(f, "Input cannot be empty")
}
AppError::ParseFailed(input) => {
write!(f, "Failed to parse '{}' as a number", input)
}
AppError::OutOfRange { value, min, max } => {
write!(f, "Value {} is out of range [{}, {}]", value, min, max)
}
AppError::DivisionByZero => {
write!(f, "Division by zero is not allowed")
}
}
}
}
// Implement From<ParseIntError> — let ? operator convert automatically
impl From<ParseIntError> for AppError {
fn from(_: ParseIntError) -> Self {
AppError::ParseFailed("unknown".into())
}
}
// Processing User Input:Analyze and Verify
fn process_input(input: &str, divisor: i32) -> Result<i32, AppError> {
if input.is_empty() {
return Err(AppError::EmptyInput);
}
let value: i32 = input.parse().map_err(|_| {
// Manually Convert Error Types
AppError::ParseFailed(input.to_string())
})?;
if value < -100 || value > 100 {
return Err(AppError::OutOfRange {
value,
min: -100,
max: 100,
});
}
if divisor == 0 {
return Err(AppError::DivisionByZero);
}
Ok(value / divisor)
}
fn main() {
let test_cases = vec![
("42", 2, "Normal case"),
("", 1, "Empty input"),
("abc", 1, "Parse error"),
("999", 1, "Out of range"),
("50", 0, "Division by zero"),
];
for (input, divisor, description) in test_cases {
match process_input(input, divisor) {
Ok(result) => println!("[{}] OK: {}", description, result),
Err(e) => println!("[{}] Error: {}", description, e),
}
}
}
出力:
[Normal case] OK: 21
[Empty input] Error: Input cannot be empty
[Parse error] Error: Failed to parse 'abc' as a number
[Out of range] Error: Value 999 is out of range [-100, 100]
[Division by zero] Error: Division by zero is not allowed
カスタムエラー型を定義するには、
Display(ユーザーに表示されるエラーメッセージ)とDebug({:?}のデバッグ出力)を実装する必要があります。From<T>トレイトを実装することで、?演算子が特定のエラー型をカスタム型に自動的に変換できるようになります。これが、?を型間で伝播させるための中核となる仕組みです。
(4) ▶ サンプル:panic! 対 エラーの戻り値への対処法 (難易度 ⭐⭐⭐)
// ============================================
// Demo panic! Scenarios Where Error Return Values Are Appropriate
// panic! → Unrecoverable error(Bug/Assertion Failed)
// Result → Recoverable errors(User Input/IOFailure)
// ============================================
// --- Suitable for panic! the scene ---
/// Read the port number from the configuration file
/// If the configuration file is missing,This is part of the program Bug,panic That makes sense.
fn get_default_port() -> u16 {
// This value is hard-coded in the code.,It's impossible for the resolution to fail.
"8080"
.parse()
.expect("Hardcoded port number is invalid")
}
/// A function that accepts only positive integers
/// Passing a negative number indicates that the caller has Bug,panic Quickly Identify Problems
fn sqrt_unchecked(x: i32) -> f64 {
if x < 0 {
panic!("sqrt_unchecked called with negative value: {}", x);
}
(x as f64).sqrt()
}
// --- Suitable for Result the scene ---
/// Parsing Numbers from User Input
/// It is normal for users to enter the wrong format.,Should be returned Result
fn parse_user_input(input: &str) -> Result<i32, String> {
input
.parse()
.map_err(|_| format!("'{}' is not a valid integer", input))
}
/// Calculate Body Mass Index(BMI)
/// Weight is 0 A negative number may indicate a data error.,Not a program Bug
fn calculate_bmi(weight_kg: f64, height_m: f64) -> Result<f64, String> {
if weight_kg <= 0.0 {
return Err("Weight must be positive".to_string());
}
if height_m <= 0.0 {
return Err("Height must be positive".to_string());
}
Ok(weight_kg / (height_m * height_m))
}
fn main() {
// Scene 1:panic Used for unrecoverable errors
println!("Default port: {}", get_default_port());
// Scene 2:panic Used for assertions——Invalid parameter
let value = 16;
println!("sqrt({}) = {}", value, sqrt_unchecked(value));
// Scene 3:Result Used for recoverable errors——User Input
let inputs = vec!["42", "hello", "-5"];
for input in inputs {
match parse_user_input(input) {
Ok(n) => println!("Parsed: {}", n),
Err(e) => println!("Parse failed: {}", e),
}
}
// Scene 4:Result Used for business logic validation
let bmi_cases = vec![
(70.0, 1.75),
(0.0, 1.70),
(65.0, -0.5),
];
for (weight, height) in bmi_cases {
match calculate_bmi(weight, height) {
Ok(bmi) => println!("BMI: {:.1}", bmi),
Err(e) => println!("BMI error: {}", e),
}
}
// Scene 5: expect's panic message helps with debugging
let numbers = vec![10, 20, 30];
let first = numbers.first().expect("Vector should not be empty");
println!("First element: {}", first);
}
出力:
Default port: 8080
sqrt(16) = 4
Parsed: 42
Parse failed: 'hello' is not a valid integer
Parsed: -5
BMI: 22.9
BMI error: Weight must be positive
BMI error: Height must be positive
First element: 10
戦略を選択するための基本原則:
panic!は「プログラム自体のバグ」(無効なハードコーディングされたデータ、規約に違反するパラメータ、配列のインデックス範囲外)に使用されます。Resultは「外部環境やユーザー入力によって引き起こされる例外」(I/Oエラー、解析エラー、ビジネス検証エラー)に使用されます。簡単に言えば:修正可能な問題にはResultを、修正不可能な問題にはpanicを使用してください!
6. anyhow および thiserror の概念の概要
本番環境向けのRustプロジェクトでは、エラー処理を簡素化するために、2つのコミュニティ作成のcrateが広く利用されています:
| クレート | 主な用途 | 活用例 | 主な機能 |
|---|---|---|---|
| いずれにせよ | エラーの伝播(呼び出し側の視点) | アプリケーションのメイン関数、CLIツール、スクリプト | anyhow::Result<T>、.context() エラーの背景情報を提供する |
| thiserror | エラーの定義(ライブラリ作成者の視点から) | ライブラリの公開APIにおけるエラーの種類 | derive マクロを使用して実装され、Display および Error を自動生成する |
// anyhow Style(Concept Examples,No execution required)
// use anyhow::{Context, Result};
//
// fn read_config() -> Result<String> {
// let content = std::fs::read_to_string("config.toml")
// .context("Failed to read config file")?;
// Ok(content)
// }
// thiserror Style(Concept Examples,No execution required)
// use thiserror::Error;
//
// #[derive(Error, Debug)]
// enum MyError {
// #[error("IO error: {0}")]
// Io(#[from] std::io::Error),
//
// #[error("Parse error: {0}")]
// Parse(#[from] std::num::ParseIntError),
// }
anyhow「エラーをどのように定義するか」ではなく、「エラーをどのように処理するか」に焦点を当てることを推奨しています;thiserror手動でのコーディングによるDisplayおよびFromの実装の代わりに、アノテーションを使用することを推奨しています。通常、この2つは組み合わせて使用されます。ライブラリではthiserrorを使用してきめ細かいエラー型を定義し、アプリケーションではanyhowを使用してそれらを統一的に伝播させます。
(1) ▶ サンプル:総合演習—多層的な誤差伝播と回復(難易度 ⭐⭐⭐)
// ============================================
// Comprehensive Example:Custom Errors + ? Dissemination + Recovery Strategy
// ============================================
use std::fmt;
#[derive(Debug)]
enum AppError {
ParseError(String),
ValidationError(String),
NotFound(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::ParseError(msg) => write!(f, "Parsing error: {}", msg),
AppError::ValidationError(msg) => write!(f, "Validation Error: {}", msg),
AppError::NotFound(msg) => write!(f, "Not found: {}", msg),
}
}
}
impl From<std::num::ParseIntError> for AppError {
fn from(e: std::num::ParseIntError) -> Self {
AppError::ParseError(e.to_string())
}
}
fn parse_age(input: &str) -> Result<u8, AppError> {
let age: u8 = input.parse().map_err(|_| AppError::ParseError(format!("'{}' Not a valid number", input)))?;
if age > 150 {
return Err(AppError::ValidationError(format!("Age {} Unreasonable", age)));
}
Ok(age)
}
fn find_user(id: u32) -> Result<String, AppError> {
let users = [(1, "Alice"), (2, "Bob"), (3, "Charlie")];
users.iter()
.find(|(uid, _)| *uid == id)
.map(|(_, name)| name.to_string())
.ok_or_else(|| AppError::NotFound(format!("User ID={}", id)))
}
fn process_user(id_str: &str, age_str: &str) -> Result<String, AppError> {
let id: u32 = id_str.parse().map_err(|_| AppError::ParseError(format!("Invalid ID: '{}'", id_str)))?;
let age = parse_age(age_str)?;
let name = find_user(id)?;
Ok(format!("User: {}, Age: {}", name, age))
}
fn main() {
let test_cases = [
("1", "30"),
("2", "200"),
("5", "25"),
("abc", "30"),
("3", "abc"),
];
println!("=== User Processing Test ===");
for (id, age) in &test_cases {
match process_user(id, age) {
Ok(result) => println!("Success: {}", result),
Err(e) => println!("Failure: {}", e),
}
}
println!("\n=== Batch Processing (Fault-Tolerant) ===");
let inputs = [("1", "30"), ("5", "25"), ("2", "abc"), ("3", "20")];
let mut success_count = 0;
for (id, age) in &inputs {
match process_user(id, age) {
Ok(result) => { println!("OK: {}", result); success_count += 1; }
Err(e) => println!("Skip: {}", e),
}
}
println!("Success: {}/{}", success_count, inputs.len());
}
出力:
=== User Processing Test ===
Success: User: Alice, Age: 30
Failure: Validation Error: Age 200 Unreasonable
Failure: Not found: User ID=5
Failure: Parsing error: Invalid ID: 'abc'
Failure: Parsing error: 'abc' Not a valid number
=== Batch Processing (Fault-Tolerant) ===
OK: User: Alice, Age: 30
Skip: Not found: User ID=5
Skip: Parsing error: 'abc' Not a valid number
OK: User: Charlie, Age: 20
Success: 2/4
カスタム
AppError列挙 +From変換 +?伝播により、エラー処理の連鎖が明確かつ簡潔になります。map_errは、根本的なエラーをカスタム型に変換します。バッチ処理では、フォールトトレランスを実現するためにmatchを使用してください。これにより、ループが中断されることなく、障害がログに記録された上で処理が継続されます。
❓ よくある質問
unwrap() は悪い慣行と見なされますか?? 演算子と match エラー処理――どちらを優先すべきか?? を優先し、きめ細かな処理が必要な場合にのみ match を使用してください。?はこれらをどのように統一的に伝播させることができますか?Fromトレイトによる自動変換を通じてです。panic! はキャッチできますか?Display と Debug の両方を実装する必要があるのですか?Error トレイトが、その両方を要求しているためです。📖 まとめ
Result<T, E>は Rust のエラー処理の中核をなすものです。Ok(T)は成功を、Err(E)は失敗を示し、コンパイラは両方のケースを処理することを要求します。?演算子は、エラー伝播のための構文糖衣であり、失敗時には自動的にErrを返し、成功時にはOkから値を取得し、エラーの型変換を自動的に行います。match/unwrap_or/unwrap_or_elseは、きめ細かい分岐から高速なフォールバックに至るまで、さまざまな粒度レベルでのエラー処理を提供します。- カスタムエラー型:
Display + Debugを実装してエラーメッセージを明確かつ読みやすくし、From traitを使用して型変換を行ってください。 panic!は回復不可能なエラー(プログラムのバグ)に使用され、Resultは回復可能なエラー(外部環境の例外)に使用されます。これが Rust における基本的なエラー処理の戦略です。anyhowおよびthiserrorは、本番環境レベルのエラー処理のための標準的なツールです。前者は伝播を簡素化し、後者は定義を簡素化します。
📝 練習問題
-
難易度 ⭐:文字列を年齢(0~150)として解析する関数
fn parse_age(input: &str) -> Result<u8, String>を作成してください。解析に失敗した場合、または値が範囲外の場合は、対応するエラーメッセージを返してください。mainでは、matchを使用して、有効な年齢、数値以外の入力、範囲外の値という 3 つのケースを処理してください。 -
難易度 ⭐⭐:関数のネストした呼び出しを含むシナリオを作成してください。関数 A が関数 B を呼び出し、関数 B が関数 C を呼び出し、各レベルでエラーが発生する可能性があります。
?演算子を使用してエラーを伝播させてください。シナリオ:read_user_file()->parse_user_data()->validate_age()。各レベルは、列挙型を使用して定義された同じカスタムエラー型UserDataErrorを返します。このエラー型には、FileNotFound、ParseFailed(String)、InvalidAge(i32)の 3 つのバリエーションが含まれます。 -
難易度 ⭐⭐⭐:
add、subtract、multiply、divideの 4 つの演算をサポートするミニ電卓を設計してください。すべての演算において、誤差は?を通じて伝播します。要件:CalcError列挙型を定義する (DivideByZero,Overflow,InvalidOperator(String))DisplayおよびDebugを実装する?を使用して、複数の操作を連結します:calculate("10 + 5 * 2")などの文字列式- ヒント:まず文字列をスペースで分割し、その後、
foldまたはループを使用して各部分を個別に処理してください。



