跳转至

0x09 Error Handling*

Rust 有很多特性来处理出现错误的情况。在很多情况下,Rust 要求程序员考虑出错的可能性,并在编译代码之前就采取行动,确保在将代码部署到生产环境之前就发现错误并正确处理。

Rust 将错误分为可恢复错误(recoverable)和不可恢复错误(unrecoverable)。可恢复错误通常代表向用户报告错误和重试操作是合理的情况,比如未找到文件。不可恢复错误通常指 bug,比如尝试访问超过数据结尾的位置。

大部分语言并不区分这两类错误,而是采用类似异常的方式统一处理。Rust 并没有异常,使用可恢复错误 Resut<T, E> 和不可恢复错误 panic!

Unrecoverable Errors with Panic*

程序执行 panic! 宏会打印出一个错误信息,展开并清理栈数据,然后退出。

所谓展开(unwinding),是指 Rust 在出现 panic 时会默认地回溯栈并清理它遇到的每一个函数的数据。不过这个回溯清理的过程还是比较耗时的,另一个选择是直接终止(abort),不清理数据就退出程序,程序使用的内存需要由操作系统来清理。可以在 Cargo.toml 文件的 [profile] 块中设置,比如在 release 模式下。

[profile.release]
panic = 'abort'

panic backtrace*

程序 panic 的输出样例如下。

thread 'main' panicked at 'crash and burn', src/main.rs:2:5
note: Run with `RUST_BACKTRACE=1` for a backtrace.

第一行显示了 panic 提供的信息,并指明了在源码中的位置。第二行提醒可以通过设置 RUST_BACKTRACE 环境变量来获取回溯列表。backtrace 是一个执行到目前为止所有被调用的函数的列表,通过阅读这个列表就可以找到问题的源头。

Recoverable Errors with Result*

大部分错误并没有严重到需要程序停止执行,回顾第二章就提到的 Result 类型,其实是一个枚举类型。

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

第十章会介绍泛型,通过泛型可以定义适用于不同场景的 Result 类型。和其他枚举一样,使用 match 表达式可以处理 Result 的不同值,甚至还可以进一步根据 Err 处理不同的错误类型。

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Problem creating the file: {:?}", e),
            },
            other_error => panic!("Problem opening the file: {:?}", other_error),
        },
    };
}

match 表达式能很好地处理多情况处理,还可以使用闭包(closure)处理,第十三章会介绍闭包, Result<T, E> 有很多接受闭包的方法,并采用 match 表达式实现。

失败是 panic 的简写:unwrap 和 expect*

match 表达式的写法虽然好理解但还是冗长,Result<T, E> 类型定义了很多辅助方法来处理各种情况。一种常见的情景:Ok 返回 Ok 中的值,Err 则 panic,可以使用 unwrap 方法来简写。如果想进一步显示错误信息,可以使用 expect 方法,并把要传给 panic 的信息传给 expect

传播错误*

当编写一个实现一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定如何处理,这成为传播(propagating)错误。调用者拥有更多信息来处理可能的错误。

use std::io;
use std::io::Read;
use std::fs::File;

fn read_username_from_file() -> Result<String, io::Error> {
    let f = File::open("hello.txt");

    let mut f = match f {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut s = String::new();

    match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        Err(e) => Err(e),
    }
}

以上代码展示了一个函数使用 match 将错误返回给代码调用者的例子。

函数要做的就是从文件中读取用户名,那么就可能出现文件不存在或无法读取的错误,这个函数需要把错误返回给调用者。

首先调用 File::open 读取文件,然后使用 match 处理返回值,如果这里就出现错误就直接返回错误。之后调用 read_to_string 方法将文件内容读取到 s 中,如果出现错误,返回错误,这里不需要 return,因为这是函数最后一个表达式。

调用者会获得一个包含用户名的 Ok 值,或者一个包含 io::ErrorErr 值。而调用者如何处理这些值就不是这里需要考虑的了,这里做的只是将所有成功或失败信息向上传播。

传播错误的简写: ? 运算符*

上面讲的传播错误的模式在 Rust 中很常见,Rust 提供了 ? 运算符来使其更易于处理。

use std::io;
use std::io::Read;
use std::fs::File;

fn read_username_from_file() -> Result<String, io::Error> {
    let mut f = File::open("hello.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}

这段代码用 ? 运算符代替了 match 表达式,那我们就知道了 ? 运算符的工作方式,如果 Result 的值是 Ok,这个表达式会返回 Ok 中的值而程序讲继续执行,如果值是 ErrErr 中的值讲作为整个函数的返回值。

二者还是有一点不同,? 运算符所使用的错误值被传递给了 from 函数,它定义于标准库的 From trait 中,其用于讲错误从一种类型转换为另一种类型。当 ? 运算符调用 from 函数时,收到的错误类型将被转换为由当前函数返回类型中所指定的错误类型。这在当函数返回单个错误类型来代表所有可能失败的方式时很有用,只要每一个错误类型都实现了 from 函数来定义如何将自身转换为返回的错误类型,? 运算符会自动处理这些转换。

甚至还可以在 ? 之后直接使用链式方式调用来进一步缩短代码。

fn read_username_from_file() -> Result<String, io::Error> {
    let mut s = String::new();

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

    Ok(s)
}

因为 ? 运算符有类似 match 表达式中 return Err(e) 的部分实现,所以要求函数返回值类型是 Result,才能与 ? 运算符兼容。如果想在不返回 Result 的函数中调用其他返回 Result 的函数时使用 ? 的话,没有其他限制的话可以修改返回类型,另一种就是通过使用 matchResult 的方法来处理。

Panic or Not*

上面学习了两种错误处理的方式,panic! 或返回 Result 枚举,下面就需要考虑如何使用,何时应该 panic! 以及何时应该返回 Result

示例、代码原型和测试适合 panic。

当有一些其他逻辑确保 Result 会是 Ok 值时,还是需要处理一个 Result 值。

use std::net::IpAddr;

let home: IpAddr = "127.0.0.1".parse().unwrap();

上面的代码,解析一个硬编码的有效的 IP,但是 parse 方法的返回值是 Result 值,还是需要处理 Result 值,这时候可以使用 unwrap 方法快捷处理,因为永远不会 panic。

错误处理指导原则*

当有可能会导致有害状态的情况下建议使用 panic!,有害状态是指当一些假设、保证、协议或不可变性被打破的状态,例如无效的值、自相矛盾的值或被传递了不存在的值。外加以下几种情况:

  • 有害状态并不包含预期会偶尔发生的错误
  • 在此之后代码的允许依赖不处于这种有害状态
  • 当没有可行的手段来讲有害状态信息编码进所使用的类型中的情况

别人调用自己的代码并传递了一个无意义的值,可以使用 panic 警告,类似的,也适用于调用不能控制的外部代码时,无法修复其返回的无效状态。

而当预期会出现错误时,返回 Result 更合适。

函数通常遵循:它们的行为只有在输入满足特定条件时才能得到保证。而违反约定时 panic 是有道理的。

虽然在函数中做各种错误检查很麻烦,但其实 Rust 和编译器的类型检查已经进行了很多检查,保证在拥有一个有效值的前提下进行代码逻辑。

总结*

Rust 的错误处理功能被设计为编写更健壮的代码。在适当的场景中使用 panic! 宏或 Result 枚举来进行错误处理。


最后更新: June 13, 2021