【Rust学习之旅】错误处理 ,“rust:恐慌了”(九)

1,286 阅读8分钟

不管是什么语言错误处理都是必须的,JavaScript 提供了try catch 捕获错误,也提供了 Error 通过throw 抛出一个错误。当然rust也提供了类似的处理机制。

错误类型

Rust 将错误分为两大类:可恢复的recoverable)和 不可恢复的unrecoverable)错误。

Rust提供了 Result<T, E> 类型,用于处理可恢复的错误,还有 panic! 宏,在程序遇到不可恢复的错误时停止执行。

对于一个可恢复的错误,比如文件未找到的错误,我们很可能只想向用户报告问题并重试操作。不可恢复的错误总是 bug 出现的征兆,比如试图访问一个超过数组末端的位置,因此我们要立即停止程序。

panic! 处理不可恢复的错误

在rust中有两种方法造成 panic:

  • 执行会造成代码 panic 的操作(比如访问超过数组结尾的内容)
  • 显式调用 panic! 宏。

通常情况下这些 panic 会打印出一个错误信息,展开并清理栈数据,然后退出。

通过一个环境变量,你也可以让 Rust 在 panic 发生时打印调用堆栈(call stack)以便于定位 panic 的原因。

对应 panic 时的栈展开或终止

当panic发生,程序处理有两种选择:

  • 程序展开调用栈,rust沿着调用栈,回溯清理每个函数的数据。
  • 立即终止调用栈,不进行清理工作,需要后续靠操作系统完成清理。

如果你需要项目的最终二进制文件越小越好,panic 时通过在 Cargo.toml 的 [profile] 部分增加 panic = 'abort',可以由展开切换为终止。例如,如果你想要在 release 模式中 panic 时直接终止:

[profile.release]
panic = 'abort'

我们可以直接使用panic!宏直接抛出一个错误。和我们JavaScript中 throw new Error(),作用一样。

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

backtrace

backtrace 是一个执行到目前位置所有被调用的函数的列表。RUST_BACKTRACE 环境变量来得到一个 backtrace。

获取backtrace,必须启用 debug 标识。当不使用 --release 参数运行 cargo build 或 cargo run 时 debug 标识会默认启用。

这里就相当于给你更详细的错误调用堆栈信息。

用 Result 处理可恢复的错误

大部分错误并没有严重到需要程序完全停止执行。JavaScript提供了try catch来给我们捕获发生的错误。rust也提供了Result 枚举类型,帮助我们处理错误。

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

当我们打开一个文件的时候,我们就可以使用match 来匹配错误

use std::fs::File;

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

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => panic!("Problem opening the file: {:?}", error),
    };
}

这里我们告诉 Rust 当结果是 Ok 时,返回 Ok 成员中的 file 值,然后将这个文件句柄赋值给变量 greeting_filematch 的另一个分支处理从 File::open 得到 Err 值的情况。在这种情况下,我们选择调用 panic! 宏。

注意与 Option 枚举一样,Result 枚举和其成员也被导入到了 prelude 中,所以就不需要在 match 分支中的 Ok 和 Err 之前指定 Result::

匹配不同的错误

上面File::open 是因为什么原因失败都会 panic!。我们真正希望的是对不同的错误原因采取不同的行为:如果 File::open 因为文件不存在而失败,我们希望创建这个文件并返回新文件的句柄。如果 File::open 因为任何其他原因失败,例如没有打开文件的权限,我们仍然希望那样发生 panic!

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!("Problem creating the file: {:?}", e),
            },
            other_error => {
                panic!("Problem opening the file: {:?}", other_error);
            }
        },
    };
}

File::open 返回的 Err 成员中的值类型 io::Error,它是一个标准库中提供的结构体。这个结构体有一个返回 io::ErrorKind 值的 kind 方法可供调用。

io::ErrorKind 是一个标准库提供的枚举,它的成员对应 io 操作可能导致的不同错误类型。

使用 unwrap_or_else

下面这段代码和上面的作用一样。但没有嵌套match,unwrap_or_else的作用在发生错误的时候执行后面的代码。没有直接返回成功的结果。

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

fn main() {
    let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("Problem creating the file: {:?}", error);
            })
        } else {
            panic!("Problem opening the file: {:?}", error);
        }
    });
}

失败时 panic 的简写:unwrap 和 expect

match 能够胜任它的工作,不过它可能有点冗长并且不总是能很好的表明其意图。

Result<T, E> 类型定义了很多辅助方法来处理各种情况。

  • 其中之一叫做 unwrap,如果 Result 值是成员 Okunwrap 会返回 Ok 中的值。如果 Result 是成员 Errunwrap 会为我们调用 panic!

  • 还有另一个类似于 unwrap 的方法它还允许我们选择 panic! 的错误信息:expect。使用 expect 而不是 unwrap 并提供一个好的错误信息可以表明你的意图并更易于追踪 panic 的根源。

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")
        .expect("hello.txt should be included in this project");
}

unwrapexpect的区别就是,expect可以自定义错误信息,unwrap不可以

传播错误

当编写一个其实先会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。这被称为 传播propagating)错误

这里展示了一个从文件中读取用户名的函数。如果文件不存在或不能读取,这个函数会将这些错误返回给调用它的代码:

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

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

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

    let mut username = String::new();

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

就像在我们JavaScript中我们也会经常利用Promise来返回一个reject一个错误,再通过catch捕获处理

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

上面我们使用match来匹配并返回了io::Error,有时候也会觉得有些麻烦,所以rust提供了更简单的方法?操作符号,可typescript中的?可选链操作符,差不多只不过,rust会默认返回一个错误。

Result 值之后的 ? 与处理 Result 值的 match 表达式有着完全相同的工作方式。如果 Result 的值是 Ok,这个表达式将会返回 Ok 中的值而程序将继续执行。如果值是 ErrErr 中的值将作为整个函数的返回值,就好像使用了 return 关键字一样

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

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

match 表达式与 ? 运算符所做的有一点不同:? 运算符所使用的错误值被传递给了 from 函数,它定义于标准库的 From trait 中。

当 ? 运算符调用 from 函数时,收到的错误类型被转换为由当前函数返回类型所指定的错误类型。这在当函数返回单个错误类型来代表所有可能失败的方式时很有用,即使其可能会因很多种原因失败。

和typescript一样支持链式调用🤔,确实这样方便了很多。

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

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

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

    Ok(username)
}
}

哪里可以使用 ? 运算符

? 运算符只能被用于返回值与 ? 作用的值相兼容的函数。因为 ? 运算符被定义为从函数中提早返回一个值

match 作用于一个 Result 值,提早返回的分支返回了一个 Err(e) 值。函数的返回值必须是 Result 才能与这个 return 相兼容。

 main 函数一般都默认返回 ()。但main 函数也可以返回 Result<(), E>

use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
    let greeting_file = File::open("hello.txt")?;

    Ok(())
}

Box<dyn Error> 类型是一个 trait 对象 后面会详细讲,这里你可以把它理解成,任意类型的错误类型,

什么时候使用panic!

要不要 panic!,在于你是否清楚的知道程序,是不是发生了不可恢复的错误,或者当前错误影响到了后续程序的执行,那我们就可以直接使用 panic!退出程序。

不然你可以返回Result类型交给调用者处理该错误。

结语

Rust 的错误处理功能被设计为帮助你编写更加健壮的代码。panic! 宏代表一个程序无法处理的状态,并停止执行而不是使用无效或不正确的值继续处理。Rust 类型系统的 Result 枚举代表操作可能会在一种可以恢复的情况下失败。可以使用 Result 来告诉代码调用者他需要处理潜在的成功或失败。在适当的场景使用 panic! 和 Result 将会使你的代码在面对不可避免的错误时显得更加可靠。

这里对比起JavaScript的错误处理机制,感觉差不多,只不过JavaScript没有这么严格。大同小异,不是一个很难理解的点。