跳转至

5. 错误处理篇

对应章节:第9章(错误处理)。
本篇掌握 Rust 的错误处理体系:Result、自定义错误、? 运算符与 JSON 持久化。


项目 12:银行账户管理器

目标:自定义错误类型 + JSON 持久化,掌握 Rust 的错误处理体系。

创建项目

cargo new bank
cd bank
cargo add serde --features derive   # serde + derive 特性
cargo add serde_json

功能要求

  1. 支持开立账户、存款、取款、查余额
  2. 余额不足、金额非法等返回自定义错误
  3. 数据用 JSON 保存到文件

知识点

知识点 说明
自定义 enum 错误 定义错误种类
Display + Error trait 让错误可打印、可传播
? 运算符 错误自动传播
map_err / ok_or_else 转换错误类型
serde 序列化/反序列化
entry().or_insert(...) HashMap 取或建

代码

依赖已由 cargo add 添加(cargo search serde 可查最新版本),无需手动编辑 Cargo.toml。

src/main.rs:

use std::collections::HashMap;
use std::fmt;
use std::fs;

// ============ 自定义错误类型 ============
// 用枚举表示"可能出现的错误种类"
#[derive(Debug)]
enum BankError {
    // 余额不足:携带余额与取款额两个字段,便于给用户清晰提示
    InsufficientFunds { balance: f64, amount: f64 },
    InvalidAmount(f64),          // 金额非法
    AccountNotFound(String),     // 账户不存在
    Io(std::io::Error),          // IO 错误
}

// 实现 Display:让错误可以用 {} 打印成可读文本
impl fmt::Display for BankError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            BankError::InsufficientFunds { balance, amount } => {
                write!(f, "余额不足:余额 {},取款 {}", balance, amount)
            }
            BankError::InvalidAmount(amount) => {
                write!(f, "金额不合法:{}", amount)
            }
            BankError::AccountNotFound(name) => {
                write!(f, "账户不存在:{}", name)
            }
            BankError::Io(err) => write!(f, "IO 错误:{}", err),
        }
    }
}

// 实现标准 Error trait:让 ? 能自动传播、并可与别的错误互转
impl std::error::Error for BankError {}

// ============ 数据模型 ============
// derive 两个宏:自动生成"序列化成 JSON"和"从 JSON 反序列化"
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct Account {
    name: String,
    balance: f64,
}

#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct Bank {
    accounts: HashMap<String, Account>,
}

impl Bank {
    fn new() -> Bank {
        Bank { accounts: HashMap::new() }
    }

    // 存款:返回 Result<f64, BankError>,余额在 Ok 里
    fn deposit(&mut self, name: &str, amount: f64) -> Result<f64, BankError> {
        if amount <= 0.0 {
            return Err(BankError::InvalidAmount(amount));   // 显式返回错误
        }
        // entry().or_insert():key 不存在则插入初始账户,返回 &mut Account
        // 一步完成"取或建",避免先查再插的两步麻烦
        let acc = self.accounts.entry(name.to_string()).or_insert(Account {
            name: name.to_string(),
            balance: 0.0,
        });
        acc.balance += amount;
        Ok(acc.balance)
    }

    // 取款
    fn withdraw(&mut self, name: &str, amount: f64) -> Result<f64, BankError> {
        if amount <= 0.0 {
            return Err(BankError::InvalidAmount(amount));
        }
        // get_mut 返回 Option<&mut Account>
        // ok_or_else:None 时用闭包构造错误并转成 Result
        // 后面接 ?:Err 时直接返回
        let acc = self.accounts
            .get_mut(name)
            .ok_or_else(|| BankError::AccountNotFound(name.to_string()))?;
        if acc.balance < amount {
            return Err(BankError::InsufficientFunds {
                balance: acc.balance,
                amount,
            });
        }
        acc.balance -= amount;
        Ok(acc.balance)
    }

    fn balance(&self, name: &str) -> Result<f64, BankError> {
        let acc = self.accounts
            .get(name)
            .ok_or_else(|| BankError::AccountNotFound(name.to_string()))?;
        Ok(acc.balance)
    }

    // ============ JSON 持久化 ============
    fn save(&self, path: &str) -> Result<(), BankError> {
        // serde_json::to_string_pretty:生成带缩进的 JSON,方便阅读
        // map_err:把 serde_json::Error 转成我们的 BankError,让 ? 统一传播
        let json = serde_json::to_string_pretty(self)
            .map_err(|e| BankError::Io(std::io::Error::other(e)))?;
        fs::write(path, json).map_err(BankError::Io)   // IO 错误也转成 BankError
    }

    fn load(path: &str) -> Bank {
        // 链式 Option:读取成功 && 解析成功才返回 Bank
        // unwrap_or_else:任一失败则回退到空银行(不 panic)
        fs::read_to_string(path)
            .ok()
            .and_then(|s| serde_json::from_str(&s).ok())
            .unwrap_or_else(Bank::new)
    }
}

fn main() {
    let mut bank = Bank::load("bank.json");

    // 存款:match 处理 Result
    match bank.deposit("小明", 100.0) {
        Ok(b) => println!("存款成功,余额 {}", b),
        Err(e) => println!("存款失败:{}", e),   // Display 让 e 可打印
    }

    // 取款超余额 → 触发自定义错误 InsufficientFunds
    match bank.withdraw("小明", 200.0) {
        Ok(b) => println!("取款成功,余额 {}", b),
        Err(e) => println!("取款失败:{}", e),
    }

    // 查不存在的账户 → AccountNotFound
    match bank.balance("小红") {
        Ok(b) => println!("余额 {}", b),
        Err(e) => println!("查询失败:{}", e),
    }

    // 保存到文件(bank.json 会出现在项目目录)
    if let Err(e) = bank.save("bank.json") {
        println!("保存失败:{}", e);
    }
}

运行

cargo run
# 存款成功,余额 100
# 取款失败:余额不足:余额 100,取款 200
# 查询失败:账户不存在:小红

第二次运行会读取 bank.json(加载上次的账户),可以观察数据持久化。

注意事项

  1. 自定义错误三件套:#[derive(Debug)] + impl Display + impl std::error::Error。? 才能把自定义错误向上传播。
  2. entry().or_insert(...):账户不存在则自动插入初始账户,一步完成"取或建"——第13章的 entry().or_default() 是它的更简写版。
  3. ok_or_else(|| ...):把 Option 转成 Result,None 时用闭包构造错误——比 unwrap_or 更专业。
  4. map_err 转换错误:IO 错误、JSON 错误都要转成 BankError::Io(...),? 才能统一传播。
  5. unwrap_or_else(Bank::new):读取或解析失败时回退到空银行,而不是 panic。

评论