Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

10. 异常处理

10.1 简介

  在程序运行过程中,可能会遇到各种异常情况,例如非法数据、文件不存在、网络连接失败、权限不足等问题。为了保证程序的稳定运行,需要主动检测这些异常情况,并采取相应的处理措施。不同于许多传统编程语言通过异常(Exception)机制隐式抛出和捕获错误,Rust 将错误处理视为程序控制流程的一部分,通过类型系统显式表示可能失败的操作,并根据错误是否具有恢复可能,将错误分为可恢复错误和不可恢复错误两类。其中,可恢复错误通常通过 Option<T> 和 Result<T, E> 类型进行表示和处理;对于无法继续执行或不应被恢复的错误,Rust 提供了 panic! 宏,用于终止当前线程的执行。

10.2 Option

  Option 用于表示值可能不存在的场景,例如查找元素失败、获取数据时键不存在等。其本质是一个枚举,包含两种状态:Some(T) 表示值存在,并携带类型为 T 的数据;None 表示值不存在,不携带任何数据。调用者可以根据返回值判断值是否存在,也可以将 Option 继续向上层传递,由调用链的上游决定如何处理。

fn find_index(numbers: &[i32], target: i32) -> Option<usize> {
    for (index, &number) in numbers.iter().enumerate() {
        if number == target {
            return Some(index);
        }
    }
    return None;
}

fn main() {
    let numbers = [10, 20, 30, 40];

    // 根据返回结果判断值是否存在,不存在则提示用户
    match find_index(&numbers, 30) {
        Some(index) => println!("找到元素,索引为 {}", index),
        None => println!("未找到元素"),
    }

    match find_index(&numbers, 50) {
        Some(index) => println!("找到元素,索引为 {}", index),
        None => println!("未找到元素"),
    }
}
shell> cargo run
找到元素,索引为 2
未找到元素

  除了显式返回 None 外,还可以通过 ? 运算符自动传播 Option。当表达式结果为 Some 时,? 会提取其中的值继续执行;当结果为 None 时,会立即结束当前函数并返回 None。? 运算符同样适用于 Result,用于自动传播错误。

fn find_index(numbers: &[i32], target: i32) -> Option<usize> {
    // 使用 ? 运算符传播 Option
    // 如果 position() 返回 Some(index),则提取索引继续执行
    // 如果返回 None,则立即结束当前函数,并返回 None
    let index = numbers.iter().position(|&number| number == target)?;
    Some(index)
}

fn main() {
    let numbers = [10, 20, 30, 40];
    
    // 由调用者处理异常
    match find_index(&numbers, 30) {
        Some(index) => println!("找到元素,索引为 {}", index),
        None => println!("未找到元素"),
    }
}
shell> cargo run
找到元素,索引为 2

  另外,Option 提供了大量的操作函数,用于判断值是否存在、获取内部值、转换数据、组合多个 Option 以及处理引用转换等场景,使开发者无需频繁使用 match 手动匹配 Some 和 None,即可完成常见的空值处理逻辑。

fn find_index(numbers: &[i32], target: i32) -> Option<usize> {
    let index = numbers.iter().position(|&number| number == target)?;
    Some(index)
}

fn main() {

    let numbers = [10, 20, 30, 40];
    let option = find_index(&numbers, 30);

    println!("判断类型:is_some={}", option.is_some());
    println!("判断类型:is_none={}", option.is_none());


    // 值为 None 时会 panic 结束程序
    println!("获取值:unwrap={}", option.unwrap());
    // 值为 None 时会返回默认值 0
    println!("获取值:unwrap_or={}", None.unwrap_or(0));
    // 值为 None 时会返回闭包中返回的值 100
    println!("获取值:unwrap_or_else={}", None.unwrap_or_else(|| 100));
    // 值为 None 时会 panic 结束程序, 并提示输出指定消息
    println!("获取值:expect={}", option.expect("数据不存在"));

    let mut value = Some(10);
    // 取出值,并将原 Option 设置为 None
    println!("取出值: take={:?}, {:?}", value.take(), value);
    println!("替换值: replace={:?}, {:?}", value.replace(20), value);
    println!("嵌套展开:flatten={:?}", Some(Some(10)).flatten());


    println!("转换值类型:map={:?}", option.map(|x| x as f64));
    // 值为 None 时返回默认值 0
    println!("转换值类型:map_or={}", None.map_or(0_f64, |x: i32| x as f64));
    // 值为 None 时返回闭包中返回的值 100
    println!("转换值类型:map_or_else={}", None.map_or_else(|| 100, |x: i32| x * 2));

    // 条件过滤,成立返回 Some,否则返回 None
    println!("条件过滤:filter={:?}", Some(10).filter(|x| *x == 0));
    println!("条件过滤:filter={:?}", Some(10).filter(|x| *x >= 0));


    println!("转换为可变:as_mut={:?}", Some(1).as_mut());
    // 转换为值引用,返回 Some(&String)
    println!("转换为引用:as_ref={:?}", Some(String::from("Rust")).as_ref());
    // 转换为解引用后的引用,返回 Some(&str)
    println!("转换为解引用后的引用:as_deref={:?}", Some(String::from("Rust")).as_deref());


    // 两个 Option 都为 Some 时返回第二个,否则都返回 None
    println!("组合调用:and: {:?}", Some(1).and(Some(2)));
    println!("组合调用:and: {:?}", Some(1).and(None::<i32>));
    println!("组合调用:and: {:?}", None::<i32>.and(Some(2)));
    println!("组合调用:and: {:?}", None::<i32>.and(None::<i32>));

    println!("组合调用:and_then={:?}", Some(1).and_then(|v| Some(v + 1)));
    println!("组合调用:and_then={:?}", Some(1).and_then(|_| None::<i32>));
    println!("组合调用:and_then={:?}", None::<i32>.and_then(|v| Some(v + 1)));
    println!("组合调用:and_then={:?}", None::<i32>.and_then(|_| None::<i32>));

    // 前面 Option 为 Some 返回前面的,否则返回后面的 Option
    println!("组合调用:or: {:?}", Some(1).or(Some(2)));
    println!("组合调用:or: {:?}", Some(1).or(None::<i32>));
    println!("组合调用:or: {:?}", None::<i32>.or(Some(2)));
    println!("组合调用:or: {:?}", None::<i32>.or(None::<i32>));

    println!("组合调用:or_else={:?}", Some(1).or_else(|| Some(2)));
    println!("组合调用:or_else={:?}", Some(1).or_else(|| None::<i32>));
    println!("组合调用:or_else={:?}", None::<i32>.or_else(|| Some(2)));
    println!("组合调用:or_else={:?}", None::<i32>.or_else(|| None::<i32>));

    // 与迭代器结合
    Some(1).iter()
        .map(|v| v.to_string())
        .for_each(|v| println!("与迭代器结合: iter={v}"));

    let names = vec![1, 2];
    let extra = Some(3);
    let other = None::<i32>;
    names.into_iter()
        .chain(extra.into_iter())
        .chain(other.into_iter())  // None 转为空迭代器,会被直接过滤
        .for_each(|item| println!("与迭代器结合:into_iter={item}"));
}
shell> cargo run
判断类型:is_some=true
判断类型:is_none=false
获取值:unwrap=2
获取值:unwrap_or=0
获取值:unwrap_or_else=100
获取值:expect=2
取出值: take=Some(10), None
替换值: replace=None, Some(20)
嵌套展开:flatten=Some(10)
转换值类型:map=Some(2.0)
转换值类型:map_or=0
转换值类型:map_or_else=100
条件过滤:filter=None
条件过滤:filter=Some(10)
转换为可变:as_mut=Some(1)
转换为引用:as_ref=Some("Rust")
转换为解引用后的引用:as_deref=Some("Rust")
组合调用:and: Some(2)
组合调用:and: None
组合调用:and: None
组合调用:and: None
组合调用:and_then=Some(2)
组合调用:and_then=None
组合调用:and_then=None
组合调用:and_then=None
组合调用:or: Some(1)
组合调用:or: Some(1)
组合调用:or: Some(2)
组合调用:or: None
组合调用:or_else=Some(1)
组合调用:or_else=Some(1)
组合调用:or_else=Some(2)
组合调用:or_else=None
与迭代器结合: iter=1
与迭代器结合:into_iter=1
与迭代器结合:into_iter=2
与迭代器结合:into_iter=3

10.3 Result

  Result 用于表示操作可能成功或失败的情况,例如文件读取失败、网络连接失败、数据解析失败等场景。其本质是一个枚举,包含两种可能状态:Ok(T) 表示操作成功,并携带类型为 T 的数据;Err(E) 表示操作失败,并携带类型为 E 的错误信息。调用者可以根据返回结果判断操作是否成功,也可以将 Result 继续向上传递,由上层调用者决定如何处理错误。

fn divide(a: i32, b: i32) -> Result<i32, &'static str> {
    if b == 0 {
        // 失败返回时可以携带错误信息
        return Err("除数不能为零");
    }
    return Ok(a / b);
}

fn main() {
    // 根据返回结果判断操作是否成功,失败则处理错误
    match divide(10, 2) {
        Ok(result) => println!("计算成功,结果为 {}", result),
        Err(error) => println!("计算失败,{}", error),
    }

    match divide(10, 0) {
        Ok(result) => println!("计算成功,结果为 {}", result),
        Err(error) => println!("计算失败,{}", error),
    }
}
shell> cargo run
计算成功,结果为 5
计算失败,除数不能为零

  在一些场景中,可能会存在多种原因导致的错误,为了方便调用者区分不同类型的错误,并针对不同错误采取相应的处理方式,通常会使用枚举为每种错误场景定义专用的错误类型。

#[derive(Debug)]
enum DivideError {
    // 除数为零错误
    DivisionByZero(&'static str),

    // 参数非法错误
    InvalidArgument(&'static str),
}

fn divide(a: i32, b: i32) -> Result<i32, DivideError> {
    // 检查参数是否合法
    if a < 0 || b < 0 {
        return Err(DivideError::InvalidArgument("参数非法,不允许负数运算"));
    }

    // 检查除数是否为零
    if b == 0 {
        return Err(DivideError::DivisionByZero("除数不能为零"));
    }

    return Ok(a / b);
}

fn main() {
    match divide(10, 0) {
        Ok(result) => println!("计算成功,结果为 {}", result),

        Err(DivideError::DivisionByZero(message)) => {
            println!("除法失败:{}", message);
        }

        Err(DivideError::InvalidArgument(message)) => {
            println!("参数错误:{}", message);
        }
    }

    match divide(-10, 2) {
        Ok(result) => println!("计算成功,结果为 {}", result),

        Err(DivideError::DivisionByZero(message)) => {
            println!("除法失败:{}", message);
        }

        Err(DivideError::InvalidArgument(message)) => {
            println!("参数错误:{}", message);
        }
    }
}
shell> cargo run
除法失败:除数不能为零
参数错误:参数非法,不允许负数运算

  在复杂的项目中,不同函数、不同模块往往各自定义了独立的错误枚举类型。为了统一管理这些分散的错误,Rust 定义了 std::error::Error trait,为不同来源的错误提供统一的类型接口,使其能够以一致的方式进行描述、传递和处理。错误枚举类型只需实现该 trait,即可纳入 Rust 的标准错误处理体系。该 trait 继承 Debug 和 Display trait,其中,Display trait 用于提供面向用户的错误描述信息,Debug trait 用于输出调试信息。

use std::error::Error;
use std::fmt;

#[derive(Debug)]
struct User {
    id: u32,
    name: String,
    balance: u32,
    is_active: bool,
}

// 用户相关错误
#[derive(Debug)]
enum UserError {
    NotFound,
    Frozen,
}

impl fmt::Display for UserError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            UserError::NotFound => write!(f, "用户不存在"),
            UserError::Frozen => write!(f, "用户已被冻结"),
        }
    }
}

// 实现 Error trait
impl Error for UserError {}


// 转账相关错误
#[derive(Debug)]
enum TransferError {
    InsufficientBalance,
    InvalidAmount,
}

impl fmt::Display for TransferError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            TransferError::InsufficientBalance => {
                write!(f, "余额不足")
            }
            TransferError::InvalidAmount => {
                write!(f, "转账金额非法")
            }
        }
    }
}

impl Error for TransferError {}


// 转账操作,将不同模块产生的错误统一抽象为 Error trait 对象返回
fn transfer_money(from_user: &mut User, to_user: &mut User, amount: u32, ) 
  -> Result<(), Box<dyn Error>> {

    // 检查用户状态
    if !from_user.is_active || !to_user.is_active {
        return Err(Box::new(UserError::Frozen));
    }

    // 检查用户是否存在
    if from_user.id == 0 || to_user.id == 0 {
        return Err(Box::new(UserError::NotFound));
    }

    // 检查金额
    if amount == 0 {
        return Err(Box::new(TransferError::InvalidAmount));
    }

    // 检查余额
    if from_user.balance < amount {
        return Err(Box::new(TransferError::InsufficientBalance));
    }

    // 执行转账
    from_user.balance -= amount;
    to_user.balance += amount;

    println!("转账成功: {} -> {}, 金额: {}", from_user.name, to_user.name, amount);
    Ok(())
}

fn main() {

    let mut user1 = User {
        id: 1,
        name: String::from("张三"),
        balance: 100,
        is_active: true,
    };

    let mut user2 = User {
        id: 2,
        name: String::from("李四"),
        balance: 100,
        is_active: true,
    };

    let mut user3 = User {
        id: 3,
        name: String::from("王五"),
        balance: 100,
        is_active: false,
    };

    match transfer_money(&mut user1, &mut user2, 50) {
        Ok(_) => println!("交易完成"),
        Err(e) => println!("交易失败: {}", e),
    }

    match transfer_money(&mut user1, &mut user2, 0) {
        Ok(_) => println!("交易完成"),
        Err(e) => println!("交易失败: {}", e),
    }

    match transfer_money(&mut user1, &mut user3, 50) {
        Ok(_) => println!("交易完成"),
        Err(e) => println!("交易失败: {}", e),
    }
}
shell> cargo run
转账成功: 张三 -> 李四, 金额: 50
交易完成
交易失败: 转账金额非法
交易失败: 用户已被冻结

  除了显式返回 Err 外,也可以通过 ? 运算符自动传播 Result。当表达式结果为 Ok 时,? 会提取其中的值并继续执行;当结果为 Err 时,会立即结束当前函数,并将错误返回给调用者。通过 ? 运算符可以避免编写大量错误判断代码,将错误交由上层调用者统一处理。

  此外,Rust 允许 main 函数返回 Result 类型,使程序入口也能够参与错误传播。当 main 返回 Ok 时,表示程序正常结束;当返回 Err 时,运行时会自动输出错误信息,并以失败状态结束程序。

fn divide(a: i32, b: i32) -> Result<i32, &'static str> {
    if b == 0 {
        // 失败返回时可以携带错误信息
        return Err("除数不能为零");
    }
    return Ok(a / b);
}

fn main() -> Result<(), Box<dyn std::error::Error>> {

    let result = divide(10, 2)?;
    println!("结果是 {:?}", result);

    let result = divide(10, 0)?;
    println!("结果是 {:?}", result);
    Ok(())
}
shell> cargo run
结果是 5
Error: "除数不能为零"

  Result 与 Option 类型一样,同样提供了丰富的操作函数,用于判断结果状态、获取内部值、转换类型以及链式组合调用等操作,避免开发者频繁使用 match 手动匹配 Ok 和 Err,从而更加简洁地完成常见的错误传播与处理逻辑。

fn divide(a: i32, b: i32) -> Result<i32, &'static str> {
    return match b != 0 {
        true => Ok(a / b),
        false => Err("除数不能为零")
    };
}

fn main() {
    let ok_result = divide(10, 2);
    let err_result = divide(10, 0);


    println!("判断类型:is_ok={}", ok_result.is_ok());
    println!("判断类型:is_err={}", err_result.is_err());


    // Ok 时返回内部值,Err 时 panic
    println!("获取值:unwrap={}", divide(10, 2).unwrap());
    // Err 时返回默认值
    println!("获取值:unwrap_or={}", err_result.unwrap_or(0));
    // Err 时返回闭包中返回的值
    println!("获取值:unwrap_or_else={}", divide(10, 0).unwrap_or_else(|_| 100));
    // Err 时 panic,并提示输出指定消息
    println!("获取值:expect={}", divide(10, 2).expect("计算失败"));


    // 获取错误值,Ok 时 panic
    println!("获取错误:unwrap_err={:?}", divide(10, 0).unwrap_err());
    // 获取错误值,Ok 时 panic,并提示输出指定消息
    println!("获取错误:expect_err={:?}", divide(10, 0).expect_err("应该报错,但没有报错"));


    // 转换成功值类型
    println!("转换值类型:map={:?}", divide(10, 2).map(|x| x as f64));
    // Err 时返回默认值
    println!("转换值类型:map_or={}", divide(10, 0).map_or(0_f64, |x| x as f64));
    // Err 时返回闭包中返回的值
    println!("转换值类型:map_or_else={}", divide(10, 0).map_or_else(|_| 100_f64, |x| x as f64));
    // 转换错误类型(Result 专用)
    println!("转换错误类型:map_err={:?}", divide(10, 0).map_err(|e| e.to_string()));


    // 转换为引用
    println!("转换为引用:as_ref={:?}", divide(10, 2).as_ref());
    // 转换为可变引用
    let mut value = divide(10, 2);
    println!("转换为可变:as_mut={:?}", value.as_mut());
    // 转换为 Option
    println!("转为 Option:ok={:?}", divide(10, 2).ok());
    println!("转为 Option:err={:?}", divide(10, 0).err());


    // 两个 Result 都为 Ok 时返回第二个,否则返回 Err
    println!("组合调用:and={:?}", divide(10, 2).and(Ok::<i32, &str>(20)));
    println!("组合调用:and_then={:?}", divide(10, 2).and_then(|v| Ok(v + 1)));
    println!("组合调用:and_then={:?}", divide(10, 2).and_then(|_| Err::<i32, &str>("计算失败")));


    // 前面 Result 为 Ok 返回前面的,否则返回后面的 Result
    println!("组合调用:or={:?}", divide(10, 0).or(Ok::<i32, &str>(200)));
    println!("组合调用:or_else={:?}", divide(10, 0).or_else(|_| Ok::<i32, &str>(100)));


    // 与迭代器结合
    let results = [Ok(1), Err("错误"), Ok(2), Ok(3)];
    results.into_iter()
        .filter_map(Result::ok)
        .for_each(|item| println!("与迭代器结合:ok={item}"));
}
shell> cargo run
判断类型:is_ok=true
判断类型:is_err=true
获取值:unwrap=5
获取值:unwrap_or=0
获取值:unwrap_or_else=100
获取值:expect=5
获取错误:unwrap_err="除数不能为零"
获取错误:expect_err="除数不能为零"
转换值类型:map=Ok(5.0)
转换值类型:map_or=0
转换值类型:map_or_else=100
转换错误类型:map_err=Err("除数不能为零")
转换为引用:as_ref=Ok(5)
转换为可变:as_mut=Ok(5)
转为 Option:ok=Some(5)
转为 Option:err=Some("除数不能为零")
组合调用:and=Ok(20)
组合调用:and_then=Ok(6)
组合调用:and_then=Err("计算失败")
组合调用:or=Ok(200)
组合调用:or_else=Ok(100)
与迭代器结合:ok=1
与迭代器结合:ok=2
与迭代器结合:ok=3

10.4 panic! 宏

  对于不可恢复错误,Rust 提供了 panic! 宏。当程序进入无法保证继续正确运行的状态时,例如内部逻辑出现严重错误、关键配置缺失,或者程序无法继续执行时等情况,可以调用 panic! 主动终止当前执行流程。panic! 会输出错误信息,并默认执行调用栈展开(stack unwinding),在展开过程中运行必要的资源清理逻辑,随后终止发生错误的线程(见多线程章节),避免程序在不确定状态下继续运行。此外,还可以通过编译配置将 panic 行为修改为直接终止进程(abort),跳过调用栈展开过程,从而减少额外开销。

fn divide(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("除数不能为零");
    }
    return a / b;
}

fn main() {
    let result = divide(10, 2);
    println!("结果是: {}", result);

    let result = divide(10, 0);
    println!("结果是: {}", result);
}
[package]
name = "app"
version = "0.1.0"
edition = "2024"

[dependencies]

[profile.release]
panic = "abort"    # panic 时直接终止程序(abort),以减少额外开销
shell> cargo run
结果是: 5
thread 'main' (4717) panicked at src/main.rs:3:9:
除数不能为零
stack backtrace:
   0: __rustc::rust_begin_unwind at /rustc/.../library/std/src/panicking.rs:689:5
   1: core::panicking::panic_fmt at /rustc/.../library/core/src/panicking.rs:80:14
   2: app::divide at ./src/main.rs:3:9
   3: app::main at ./src/main.rs:12:18
   4: core::ops::function::FnOnce::call_once at rustc/.../core/src/ops/function.rs:250:5

shell> cargo run --release
结果是: 5
thread 'main' (5160) panicked at src/main.rs:3:9:
除数不能为零
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Aborted (core dumped)