Rust权威指南之错误处理

一. 简述

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

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

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

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

二. 不可恢复错误

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

fn main() {
    panic!("crash and burn");
}

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

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

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.

三. 可恢复错误

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

3.1. Result枚举

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

pub enum Result<T, E> {
  Ok(T),
  Err(E),
}

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

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

此时需要注意这个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>;   

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

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

这里如果发生错误都会直接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),
    },
};

上面的多个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);
    }
});

注意:如果需要查看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:?}")
    }
    
    

    针对上面的例子,我们使用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();
    
  • 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("文件不存在");
    

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)
    }
}

虽然我们通过上面的代码实现了返回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)
}

正如我们看到的,通过放置的代码末尾的?实现了我们上面的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)
}

是不是非常的nice!

3.3. main函数的Result

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

fn main() -> Result<(), Box<dyn Error>> {
    read_file_plus()?;
    Ok(())
}

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

四. Result常用方法

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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值