404 Not Found

404 Not Found


nginx

Rustのエラー処理:`Result`と`?`演算子

エラー処理は、Rustの最も注目すべき設計上の特徴の一つです。例外に依存せず、代わりに型システムを用いて「潜在的な失敗」を戻り値の型にエンコードし、コンパイラがエラーを見落とすことのないよう保証します。

Rustにはtry-catchthrowはありません。その代わりに、Result<T, E>列挙型と?演算子が使用されます。これらにより、エラー処理と通常のビジネスロジックが分離され、安全かつ簡潔なコードが実現されます。


1. テイクアウトの注文から受け取りまでのトラブル対応の体験談

(1) 実際のフードデリバリーのプロセス

シャオ・ミンは今日残業をしているため、テイクアウトを注文することにした。その過程には、問題が発生する可能性のある点がいくつかある:

手順 通常の処理 発生しうるエラー
1. レストランを探す フードデリバリーアプリを開いてレストランを検索する レストランが見つからない
2. メニューの読み込み メニュー項目の閲覧 メニューの読み込みに失敗しました(ネットワークタイムアウト)
3. 支払いを送信 支払う 支払いに失敗しました(残高不足)
4. 準備待ち 30分待つ 店舗が注文をキャンセル
5. 受け取り テイクアウトの受け取り 受け取り時に注文がキャンセルされていたことに気づく

各ステップは成功(Ok)するか、失敗(Err)する可能性があります。これをRustの手法を用いてモデル化してみましょう:

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による高速評価、およびパニックを示しています:

100%
graph TB
    A["Result&lt;T, E&gt;"] --> 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. 学習内容


4. 基本概念

100%
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」の扱い(難易度 ⭐)

RUST
// ============================================
// 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");
}

出力:

TEXT
=== 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 は最も包括的な処理を提供します。つまり、OkErr に対して個別の処理ロジックを記述することができます。unwrap_orunwrap_or_else はショートカットであり、失敗した場合のデフォルト値を提供します。unwrap() は最もリスクが高い方法です。これは成功が保証されていると仮定しており、失敗した場合は即座にクラッシュします。


(2) ▶ サンプル:? 演算子—連鎖的な誤差の伝播 (難易度 ⭐⭐)

RUST
// ============================================
// ? 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 が存在し、かつ空でない場合):

TEXT
--- 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) ▶ サンプル:カスタムエラー型 — 表示とデバッグの実装 (難易度 ⭐⭐)

RUST
// ============================================
// 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),
        }
    }
}

出力:

TEXT
[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! 対 エラーの戻り値への対処法 (難易度 ⭐⭐⭐)

RUST
// ============================================
// 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);
}

出力:

TEXT
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 を自動生成する
RUST
// 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) ▶ サンプル:総合演習—多層的な誤差伝播と回復(難易度 ⭐⭐⭐)

RUST
// ============================================
// 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());
}

出力:

TEXT
=== 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 を使用してください。これにより、ループが中断されることなく、障害がログに記録された上で処理が継続されます。


❓ よくある質問

Q unwrap() は悪い慣行と見なされますか?
A はい。エラーが発生しないと確信できる場合を除き、そう見なされます。
Q ? 演算子と match エラー処理――どちらを優先すべきか?
A エラーの伝播については ? を優先し、きめ細かな処理が必要な場合にのみ match を使用してください。
Q 私の関数には複数のエラータイプ(IOエラー、解析エラー、ビジネスエラー)が含まれています。?はこれらをどのように統一的に伝播させることができますか?
A Fromトレイトによる自動変換を通じてです。
Q panic! はキャッチできますか?
A はい、ただし標準的なエラー処理手法として使用すべきではありません。
Q カスタムエラー型は、なぜ DisplayDebug の両方を実装する必要があるのですか?
A Rust の Error トレイトが、その両方を要求しているためです。

📖 まとめ


📝 練習問題

  1. 難易度 ⭐:文字列を年齢(0~150)として解析する関数 fn parse_age(input: &str) -> Result<u8, String> を作成してください。解析に失敗した場合、または値が範囲外の場合は、対応するエラーメッセージを返してください。main では、match を使用して、有効な年齢、数値以外の入力、範囲外の値という 3 つのケースを処理してください。

  2. 難易度 ⭐⭐:関数のネストした呼び出しを含むシナリオを作成してください。関数 A が関数 B を呼び出し、関数 B が関数 C を呼び出し、各レベルでエラーが発生する可能性があります。? 演算子を使用してエラーを伝播させてください。シナリオ:read_user_file() -> parse_user_data() -> validate_age()。各レベルは、列挙型を使用して定義された同じカスタムエラー型 UserDataError を返します。このエラー型には、FileNotFoundParseFailed(String)InvalidAge(i32) の 3 つのバリエーションが含まれます。

  3. 難易度 ⭐⭐⭐addsubtractmultiplydivide の 4 つの演算をサポートするミニ電卓を設計してください。すべての演算において、誤差は ? を通じて伝播します。要件:

    • CalcError 列挙型を定義する (DivideByZero, Overflow, InvalidOperator(String))
    • Display および Debug を実装する
    • ? を使用して、複数の操作を連結します:calculate("10 + 5 * 2") などの文字列式
    • ヒント:まず文字列をスペースで分割し、その後、fold またはループを使用して各部分を個別に処理してください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%