• Rust权威指南之错误处理


    一. 简述

    Rust中,我们将错误分为两大类:可恢复错误和不可恢复错误。

    • 可恢复错误:例如文件未找到等,一般需要它们报告给用户并再次尝试进行操作;

    • 不可恢复错误:这类错误往往就是Bug的另一种说法,比如尝试访问超出数组结尾的位置等;

    Rust种没有类似的异常机制,但它提供了用于可恢复错误的类型Result,以及在程序出现不可恢复错误时中止运行的panic!宏。

    二. 不可恢复错误

    Rust种提供了一个特殊的panic!宏。程序会在panic!宏执行时打印出一段错误提示信息,展开并清理当前的调用栈,然后退出程序。

    fn main() {
        panic!("crash and burn");
    }
    
    • 1
    • 2
    • 3

    此时运行时,会看到如下所示输出:

    Finished dev [unoptimized + debuginfo] target(s) in 0.10s
         Running `target\debug\rust-qwzn.exe`
    thread 'main' panicked at 'crash and burn', src\main.rs:2:5
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
    
    • 1
    • 2
    • 3
    • 4

    panic发生时,程序会默认开始展开。这意味着Rust会沿着调用栈的反向顺序遍历所有调用函数,并依次清理这些函数中的数据。加入项目需要最终二进制包尽可能的小,那么你可以通过在Cargo.toml文件中的[profile.release]区域添加panic = 'abort'panic的默认行为从展开切换为终止。

    在上面我们可以看到我们可以通过设置环境变量RUST_BACKTACE输出回溯信息。这个和其他的编程语言差不多,如下:

    yuelong@yuelongdeMBP rust-example % RUST_BACKTRACE=1 cargo run
        Finished dev [unoptimized + debuginfo] target(s) in 0.01s
         Running `target/debug/rust-example`
    thread 'main' panicked at 'hello', src/main.rs:3:5
    stack backtrace:
       0: rust_begin_unwind
                 at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/std/src/panicking.rs:584:5
       1: core::panicking::panic_fmt
                 at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/panicking.rs:142:14
       2: rust_example::main
                 at ./src/main.rs:3:5
       3: core::ops::function::FnOnce::call_once
                 at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/ops/function.rs:248:5
    note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    三. 可恢复错误

    在日常开发中,大部分错误其实都没有严重到需要整个程序停止运行的地步。函数常常会由于一些可以简单解释并做出响应的原因而运行失败。

    3.1. Result枚举

    此时我们介绍一个枚举Result,在其中定义了两个变体:OkErr,如下:

    pub enum Result<T, E> {
      Ok(T),
      Err(E),
    }
    
    • 1
    • 2
    • 3
    • 4

    下面举一个使用Result的场景,文件的打开:

    let result = File::open("hello.txt"); // result类型时Result
    
    • 1

    此时需要注意这个Resultio::Result,它是通过类型别名重新定义的:

    pub fn open<P: AsRef<Path>>(path: P) -> io::Result<File> {
        OpenOptions::new().read(true).open(path.as_ref())
    }
    // 类型别名    
    pub type Result<T> = result::Result<T, Error>;   
    
    • 1
    • 2
    • 3
    • 4
    • 5

    此时说明File::Open调用可能成功,并返回读写文件的句柄;但是调用通常可能会失败,例如文件不存在。这时File::Open函数就可以通过Result枚举通知用户是否调用成功。

    let f = match File::open("hello.txt") {
        Ok(file) => file,
        Err(error) => panic!("打开文件发生错误:{}", error),
    };
    
    • 1
    • 2
    • 3
    • 4

    这里如果发生错误都会直接panic比较粗糙,这里我们还可以在使用一个match匹配多种错误:

    match File::open("hello.txt") {
        Ok(file) => file,
        // 打开发生错误,匹配具体错误
        Err(error) => match error.kind() {
            // 文件不存在错误,则创建文件
            ErrorKind::NotFound => match File::create("hello.txt") {
                // 创建成功,则返回
                Ok(fc) => fc,
                Err(e) => panic!("尝试创建hello.txt文件失败:{:?}", e)
            },
            // 其他错误不做处理,直接panic
            other => panic!("打开文件发生错误:{:?}", other),
        },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    上面的多个match有点套娃的意思,下面我们使用闭包优化下代码,如果文件打开失败,判断是否是文件不存在的错误,如果是则创建文件:

    File::open("hello.txt").map_err(|error| { // 闭包map_err可用来处理错误,并传递成功结果
        if error.kind() == ErrorKind::NotFound {
            // 闭包unwrap_or_else,可以用来返回成功的结果,或者发生错误后从闭包中结算得到结果返回
            File::create("hello.txt").unwrap_or_else(|error| { panic!("尝试创建hello.txt文件失败:{:?}", error); });
        } else {
            panic!("打开文件发生错误:{:?}", error);
        }
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    注意:如果需要查看map_errunwrap_or_else的具体使用可以查看标准库

    有时候我们不需要这么冗长的错误处理,下面我们介绍下更加快捷的处理方式:unwrapexpect

    • unwrap:实现match表达式的效果;当Result的返回值是Ok变体时,unwrap就会返回Ok内部的值;而当Result的返回值是Err变体时,unwrap则会替我们调用panic!宏。可以看源码实现:

      pub fn unwrap(self) -> T where E: fmt::Debug {
          match self {
              Ok(t) => t,
              Err(e) => unwrap_failed("called `Result::unwrap()` on an `Err` value", &e),
          }
      }
      // 输出panic
      fn unwrap_failed(msg: &str, error: &dyn fmt::Debug) -> ! {
          panic!("{msg}: {error:?}")
      }
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11

      针对上面的例子,我们使用unwrap可以出现如下效果:

      // thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:5:40
      let file = File::open("hello.txt").unwrap();
      
      • 1
      • 2
    • expect:它允许我们在unwrap基础上,指定panic!宏所附带的错误信息,这样可以方便我们追踪错误。

      // thread 'main' panicked at '文件不存在: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:5:40
      let file = File::open("hello.txt").expect("文件不存在");
      
      • 1
      • 2

    3.2. 返回Result

    当我们编写的函数中包含一些可能会执行失败的调用时,除了可以在函数中处理这个错误,还可以将这个错误返回给调用者,它们决定应该如何做进一步的处理。这个过程也叫做传播错误。下面看一个例子:

    fn read_file() -> Result<String, io::Error> {
        // 打开文件
        match File::open("hello.txt") {
            // 打开成功
            Ok(mut file) => {
                // 创建一个可变字符串
                let mut buf = String::new();
                // 将文件数据读入buf中,并返回
                match file.read_to_string(&mut buf) { 
                    Ok(_) => Ok(buf),
                    // 错误则返回Err
                    Err(e) => Err(e)
                }
            },
            Err(e) => Err(e)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    虽然我们通过上面的代码实现了返回Result的目的,但是还是有点冗长;恰恰Rust又为我们提供了一个快捷方式:?。下面我们看看使用?如何简化我们上面的代码:

    fn read_file_plus() -> Result<String, io::Error> {
        let mut file = File::open("hello.txt")?;
        let mut buf = String::new();
        file.read_to_string(&mut buf)?;
        Ok(buf)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    正如我们看到的,通过放置的代码末尾的?实现了我们上面的match过程功能。假如这个Result的值是Ok,那么包含在Ok中的值就会作为这个表达式的结果返回并继续执行程序。假如值是Err,那么这个值就会作为整个程序的结果返回,这样就和使用return一样的将错误传播给了调用者。

    其实上面的代码还是有点多,我们可以在?后面进行链式调用:

    fn read_file_plus() -> Result<String, io::Error> {
        let mut buf = String::new();
        File::open("hello.txt")?.read_to_string(&mut buf)?;
        Ok(buf)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    是不是非常的nice!

    3.3. main函数的Result

    我们知道main函数是一个特殊的函数,我们一般写main函数是没有写返回值的(main函数默认的返回值类型是()),那么main是否可以使用Result呢?答案是肯定的:

    fn main() -> Result<(), Box<dyn Error>> {
        read_file_plus()?;
        Ok(())
    }
    
    • 1
    • 2
    • 3
    • 4

    这里又出现了Box,我们很纳闷!不用着急后面章节会详细介绍。现在我们只需要知道Boxtrait对象(可以看作是接口),表示任何可能的错误类型。在拥有这种返回类型的main函数中使用?运算符是合法的。

    四. Result常用方法

    下面我们列举一些Result的常用方法:

    方法描述
    unwrap返回包含 self 值的包含的 Ok值。
    expect返回包含 self 值的包含的 Ok 值。
    and_then如果结果为 Ok,则调用 op,否则返回 selfErr 值。该函数可用于基于 Result 值的控制流。
    map通过对包含的 Ok值应用函数,将 Err值 Maps 转换为 Result,而保持 Err值不变。该函数可用于组合两个函数的结果。
    okResult 转换为 Option。将 self 转换为 Option,使用 self,并丢弃错误 (如果有)
    or如果结果为 Err,则返回 res; 否则,返回 selfOk
    errResult 转换为 Option。将 self 转换为 Option,使用 self,并丢弃成功值 (如果有)
    as_ref&Result 转换为 Result<&T, &E>。产生一个新的 Result,其中包含对原始引用的引用,并将原始保留在原处。

    更多的方法可以到标准库文档中查看:https://rustwiki.org/zh-CN/std/result/enum.Result.html

    这里我们再将文件读取的例子在重写下,尽量使用更多的Result中的方法:

    use std::error::Error;
    use std::fs::File;
    use std::io::{Read};
    
    fn read_file_plus(name: &str) -> Result<i32, String> {
        // 打开文件
        File::open(name)
            // 重新定义err的内容
            .map_err(|err| err.to_string())
            // 使用and_then处理文件内容
            .and_then(|mut file| {
                let mut buf = String::new();
                // 读取文件内容
                file.read_to_string(&mut buf)
                    // 重新定义err的内容
                    .map_err(|err| err.to_string())
                    // 返回buf内容
                    .map(|_| buf)
            }).and_then(|content| { // 处理内容
                content.trim().parse::<i32>() // 先去掉空格
                    .map_err(|err| err.to_string()) // 定义错误
            }).map(|n| 2 * n) // 将转换为i32的数字乘2
    }
    
    fn main() -> Result<(), Box<dyn Error>> {
        let i = read_file_plus("hello.txt")?;
        println!("{}", i); // 4
        Ok(())
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    Result和Option这两个枚举有很多相似的方法,可以比较学习下。

  • 相关阅读:
    【Android Gradle 插件】Gradle 扩展属性 ① ( Gradle 扩展属性简介 | Gradle 自定义 task 任务示例 )
    unity 点击3D物体
    数据卷(Data Volumes)&dockerfile
    python初级学习
    【内网穿透】公网远程访问本地硬盘文件
    【bug】内存占用过高,当前为[61G]
    第十二章 Spring MVC 框架扩展和SSM框架整合(2023版本IDEA)
    Linux内核源码分析 (B.7)深入理解 slab cache 内存分配全链路实现
    HTML5期末考核大作业,网站——旅游景点。 学生旅行 游玩 主题住宿网页
    【1++的数据结构】之哈希(一)
  • 原文地址:https://blog.csdn.net/yhflyl/article/details/128124821