第十一章 错误处理

在软件中出现错误是一件很正常的事,Rust有许多处理错误的方式,大部分情况下,Rust要求用户在编译前判断出现错误的可能并给出相应的举措。这项要求使得你的程序更加健壮,在部署到生产环境前可以发现错误并得到恰当的处理。

Rust将错误分为两类:

  • 可恢复错误:例如文件未找到,大部分对应措施是将问题报告给用户或者重试。
  • 不可恢复错误:例如硬件不可用,对应的措施是退出程序的运行。

其他编程语言一般不区分这两种情况,使用相同的对应措施,一般这种措施称之为异常处理机制。Rust没有这种机制,它使用Result<T,E>处理可恢复错误,使用panic!宏处理不可恢复错误。

11.1 不可以恢复的错误使用panic!宏

panic!用于处理不可恢复的错误。当程序遇到无法或不应该继续执行的错误时,会触发panic,导致程序终止执行。

  • panic的使用场景
  1. 程序不变式被违反
  2. 关键配置缺失
  3. 程序状态不一致
  4. 测试中的预期失败
  5. 程序启动时的错误
  6. 硬件错误

一般panic有两种情况,一种是用户手动写panic!宏抛出错误,另外一种是程序运行中发生了不可恢复的错误,自动抛出的。这就要求程序员在编写程序的时候能够识别出可能发生错误的地方,并给出对应的处理措施。

11.1.1 手动触发panic

rust 复制代码
fn main() {

    //最简单的方式

    panic!("出错了!");

}

这是手动抛出panic的最简单方式,直接调用panic!宏抛出。下面的示例是格式化输出错误信息。

11.1.2 系统自动抛出panic

rust 复制代码
fn main() {

    let z = divide(5, 0);

    println!("{z}");

}

fn divide(x: i32, y: i32) -> i32 {

    x / y

}

上面的示例中除数y被传入参数0,导致系统自动抛出panic。

rust 复制代码
fn main() {

    let arr = vec![1, 2, 3];

    let r = get_element(&arr, 10);

    println!("{r}");

}

fn get_element(arr: &[i32], index: usize) -> i32 {

    arr[index]

}

上面的示例中由于访问了数组不存在的下标索引值,因此系统会报错。

11.1.3 panic的展开(unwinding)和终止(about)

这是Rust编程语言对panic的两种处理策略,默认使用展开,可以在Cargo.toml文件中配置为终止,如下所示:

rust 复制代码
// 在 Cargo.toml 中配置:

[profile.release]

panic = 'abort'  // 发布版本使用终止策略,减小二进制大小

fn main() {

    // Rust默认使用展开(unwinding),但是可以配置为终止(abort)

    // 在Cargo.toml文件中配置:

    // [profile.release]

    // panic='abort'   //发布版本使用终止策略,减少二进制大小

    demonstrate_unwinding();

}

fn demonstrate_unwinding() {

    println!("函数开始");

    let v = vec![1..100];

    //let element = v[1000];

    panic!("数组下标超过了!");

    println!("这一行不会被执行");

}

使用 cargo run --release编译上面的代码,会减少代码的大小。

展开过程的展示

rust 复制代码
fn layer3() {

    println!("进入layer3");

    panic!("layer3中的错误");

    println!("离开layer3");

}

fn layer2() {

    println!("进入layer2");

    layer3();

    println!("离开layer2");

}

fn layer1() {

    println!("进入layer1");

    layer2();

    println!("离开layer1");

}

fn main() {

    use std::panic;

    println!("测试主程序开始!");

    let result = panic::catch_unwind(|| {

        layer1();

    });

    match result {

        Ok(_) => println!("程序正常运行完成!"),

        Err(_) => println!("捕获到panic,程序继续执行"),

    }

    println!("程序运行结束!");

}         

11.1.4 适合使用panic的场景-测试场景

有时测试场景下,只需要捕获的错误就可以终止程序的执行了,可以使用panic。

rust 复制代码
pub fn divide(left: u64, right: u64) -> u64 {

    left / right

}


#[cfg(test)]

mod tests {

    use super::*;

    #[test]

    fn test_divide_by_zero() {

        let result = divide(12, 0);

        assert_eq!(result, 4);

    }

    #[test]

    fn test_setup_config() {

        setup_config();

    }

    #[test]

    fn test_circle_new(){

        let cirlce = Circle::new(-10.8);

    }

    #[test]

    fn test_circle_set_radius(){

        let mut circle = Circle::new(10.1);

        circle.set_radius(15.2);

    }

}

fn setup_config() {

    let required_env="DATABASE_URL";

     match std::env::var(required_env) {

        Ok(url) => {

            if url.is_empty() {

                panic!("环境变量{required_env}不能为空");

            }

            println!("数据库url是:{required_env}");

        }

        Err(_) => {

            panic!("当前环境没有设置环境变量:{required_env}");

        }

     }

}

pub struct Circle {

    radius: f64,

}

impl Circle {

    fn new(radius:f64)->Circle {

        assert!(radius>0.0,"半径必须大于零:{}",radius);

        Circle{radius}

    }

    fn set_radius(&mut self, radius: f64) {

        let old_radius = self.radius;

        self.radius = radius;

        assert!{self.radius>0.0,"半径必须大于零:{}",radius};

    }

}

panic不适合用于出现错误后,还需要继续系统的正常运行时场景。这类错误应该使用Result或Option枚举。

  • 11.1.5 panic相关的宏和函数
  • assert序列宏
rust 复制代码
fn process_data(value: i32) {

    //基本断言

    assert!(value >= 0, "值不能为负数{value}");


    //相等断言

    let expected = 42;

    assert_eq!(value, expected, "期待{expected},实际{value}");


    //不相等断言

    assert_ne!(value, 0, "值不能为0");


    //调试断言(只在调试编译时检查)

    debug_assert!(value < 1000, "值过大:{value}");

}


fn divide_assert(a: i32, b: i32) -> i32 {

    assert!(b != 0, "除数不能为0");

    a / b

}

fn main() {

    println!("10/5={}", divide_assert(10, 5));

    process_data(42);

    println!("10/0={}", divide_assert(10, 0));

    process_data(20);

    process_data(-20);

    process_data(10020);

}

程序演示了assert的四种用法,分别是:

  1. 基本断言
  2. 相等断言
  3. 不相等断言
  4. 提示断言
  • unreachable和unimplemented宏
rust 复制代码
fn handle_status(code: u32) -> String {

    match code {

        200 => "OK".to_string(),

        404 => "Not Found".to_string(),

        500 => "Internal Server Error".to_string(),

        _ => unreachable!("不应该到达这里:code={code}"),

    }

}


//占位实现

fn process_payment(amount: f64) -> Result<(), String> {

    //开发中临时使用

    //unimplemented("支付功能尚未实现,敬请期待!");

    //或者

    //todo!("请实现支付功能的处理逻辑");

    //临时返回成功

    Ok(())

}

fn main() {

    process_payment(100.5);

    handle_status(20);

}

上面的示例展示两种宏的使用场景:

  1. unreachable!:程序不应该到达此处
  2. umimplemented!:程序还没有完成该功能,只是作为占位符使用。
  • 捕获和处理panic
rust 复制代码
use std::panic;

fn risky_operation() -> i32 {

    let numbers = vec![1, 2, 3, 4];

    let index = 10;

    if index >= numbers.len() {

        panic!("索引越界:{}", index);

    }

    numbers[index]

}

fn safe_wrapper() -> Result<i32, String> {

    let result = panic::catch_unwind(|| risky_operation());

    match result {

        Ok(value) => Ok(value),

        Err(_) => Err("操作失败,已捕获panic".to_string()),

    }

}

fn main() {

    match safe_wrapper() {

        Ok(value) => println!("结果:{value}"),

        Err(e) => println!("错误:{e}"),

    }

}

使用catch_unwind函数可以捕获panic,阻止程序中断,并对其错误做出对应的处理。

11.2 可恢复的错误使用Result

在Rust中,绝大部分错误都是可以恢复或跳过的,不需要终止程序的执行,因此需要一种可以恢复的错误,并且抛出错误的原因。例如:尝试打开一个文件,如果该文件不存在,可以尝试新建一个文件。Result枚举类型有此而生。

rust 复制代码
enum Result<T,E>{

    Ok(T),      // 成功时返回的值

    Err(E),     // 失败时返回的值

}

T:成功时返回的值的类型(例如文件的句柄std::fs::File)。

E:失败时返回值的类型(例如 std::io::Error)。

11.2.1 对Result枚举的处理方式:match

当你调用文件打开的处理函数时(File::open),将会有两种可能性,成功或失败,而Result可以同时代表这两种状态。match根据Result的返回类型来进行相应的处理。

rust 复制代码
use std::fs::File;

use std::io::ErrorKind;


fn main() {

    let greeting_file_result = File::open("hello.txt");

    let greeting_file = match greeting_file_result {

        Ok(file) => file,

        Err(error) => match error.kind() {

            ErrorKind::NotFound => match File::create("hello.txt") {

                Ok(fc) => fc, //文件不存在就创建一个新文件

                Err(e) => panic!("无法创建文件:{:?}", e),

            },

            other_error => {

                panic!("无法打开文件:{:?}", other_error);

            }

        },

    };

    println!("程序运行完毕!");

}

解析:

  1. 外层match:判断File::open执行的结果。
  2. Ok(file)分支:操作成功,直接使用句柄。
  3. Err(error)分支:操作失败,进一步分析原因。
  4. 内层match:使用error.kind()判断具体错误类型(如NotFound),并采取不同的补救措施(如创建文件)

11.2.2 使用unwrap和expect简化代码

使用match可以进行精确的匹配结果,但是代码显得非常冗长。Rust提供了两种快捷方式:

  • unwrap()

行为:如果结果是Ok,返回其值,如果是Err,直接调用panic!宏。

使用场景:确定操作绝对不会失败,或者失败是直接崩溃也无所谓的情况。

rust 复制代码
let greeting_file = File::open("hello.txt").unwrap();
  • expect()推荐使用

行为:与unwrap()类似,但是允许你自定义panic!时的报错信息。

适用场景:生产代码中,为了更好的可读性和调试体验,建议优先使用expect。

rust 复制代码
let greeting_file = File::open("hello.txt").expect("hello.txt没有找到该文件!");

11.2.2 传播错误:?运算符

当一个函数内部的操作失败时,通常更好的做法是将错误信息返回给调用者,让调用者决定如何处理。这种机制称之为"传播错误"。

  • 传统写法
rust 复制代码
//传统写法

fn read_username_from_file() -> Result<String, std::io::Error> {

    let mut f = match File::open("username.txt") {

        Ok(file) => file,

        Err(e) => return Err(e),

    };

    let mut s = String::new();

    let result = match f.read_to_string(&mut s) {

        Ok(username) => Ok(s),

        Err(e) => Err(e),

    };

    result

}
  • 使用?运算符的写法(更加简洁)
rust 复制代码
//使用?操作符

fn read_username_from_file2() -> Result<String, std::io::Error> {

    let mut f = File::open("username.txt")?;

    let mut s = String::new();

    f.read_to_string(&mut s)?;

    Ok(s)

}

?操作符的工作原理:

如果值是Ok(v),它进行解包并返回v。

如果结果是Err(e),它立即从当前函数返回Err(e),实现错误的向上传播。

  • 链式调用(终极简写)
rust 复制代码
//链式调用(终极简写)

fn read_username_from_file3() -> Result<String, std::io::Error> {

    let mut s = String::new();

    File::open("username.txt")?.read_to_string(&mut s)?;

    Ok(s)

}
  • 使用标准库函数
rust 复制代码
//使用标准库函数

fn read_username_from_file4() -> Result<String, std::io::Error> {

    std::fs::read_to_string("username.txt")

}
  • ?操作符的使用限制

注意:?操作符只能用在返回类型为Result和Option的函数中使用。如果函数的返回类型不是,则编译器会报错。

rust 复制代码
fn main() {

    use std::fs::File;

    //编译错误!main函数默认返回空元祖类型。

    let f = File::open("hello.txt!")?;

    println!("程序运行完成!");

}

解决方案,修改main函数的签名,使其返回Result类型。

rust 复制代码
fn main() -> Result<(), Box<dyn std::error::Error>> {

    use std::fs::File;

    let f = File::open("hello.txt")?;

    Ok(())

}

11.3 何时 panic!,何时 Result**?**

|-----------------------|--------------------|---------------|
| 场景 | 推荐方式 | 理由 |
| 示例、原型代码​ | unwrap()/ expect() | 快速编写,关注核心逻辑 |
| 可恢复错误(文件不存在、网络超时) | Result+ ? | 让调用者决定如何处理 |
| 不可恢复错误(Bug、逻辑错误) | panic! | 程序状态已损坏,应立即停止 |
| 库的公共 API​ | Result | 给予调用者最大的控制权 |

通过掌握 Result和 ?运算符,你将能写出既安全又符合 Rust 生态习惯的健壮代码。

相关推荐
叼烟扛炮1 小时前
C++ 知识点12 构造函数
开发语言·c++·算法·构造函数
焗猪扒饭1 小时前
极简案列入门golang依赖注入工具wire
后端·go
Byte Wizard1 小时前
C语言指针深入浅出4
c语言·开发语言
asdfg12589631 小时前
Java 大型项目设计的“内功心法”---面向对象和接口编程
java·开发语言
叼烟扛炮1 小时前
C++第八讲:string 类
开发语言·c++·算法·string
ch.ju1 小时前
Java programming Chapter Three——Array
java·开发语言
努力努力再努力wz2 小时前
【Qt入门系列】第一个 Qt Widgets 程序:项目创建、UI 文件、Hello World、对象树与 qDebug 日志
java·c语言·开发语言·数据结构·c++·qt·ui
M ? A2 小时前
Vue 转 React | VuReact 实时监听开发指南
前端·vue.js·后端·react.js·面试·开源·vureact
电子云与长程纠缠2 小时前
UE5 GameFeature创建与使用
开发语言·学习·ue5·游戏引擎