基础篇-错误处理

  • 木头
  • 更新于 2023-02-28 16:38
  • 阅读 1769

可恢复和不可恢复的错误

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

大多数语言并不区分这两种错误,并采用类似异常这样方式统一处理他们。Rust 没有异常。相反,它有Result<T, E>类型,用于处理可恢复的错误,还有 panic!宏,在程序遇到不可恢复的错误时停止执行。

panic!不可恢复的错误

突然有一天,代码出问题了,而你对此束手无策。对于这种情况,Rustpanic!宏。在实践中有两种方法造成 panic:执行会造成代码 panic的操作(比如访问超过数组结尾的内容)或者显式调用panic! 宏。这两种情况都会使程序 panic。通常情况下这些 panic 会打印出一个错误信息,展开并清理栈数据,然后退出。

fn guess(n: i32) -> bool {
    if n < 1 || n > 10 {
        panic!("Invalid number: {}", n);
    }
    n == 5
}

fn main() {
    guess(11);
}

错误:

 $ cargo run
   Compiling variables v0.1.0 (/projects/variables)
    Finished dev [unoptimized + debuginfo] target(s) in 0.63s
     Running `target/debug/variables`
thread 'main' panicked at 'Invalid number: 11', src/main.rs:3:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

panic会导致当前线程结束,甚至是整个程序的结束,这往往是不被期望看到的结果。当然当程序遇到了致命的bugpanic是最优选择。

Result可恢复的错误

大部分错误并没有严重到需要程序完全停止执行。有时,一个函数会因为一个容易理解并做出反应的原因失败。例如,如果因为打开一个并不存在的文件而失败,此时我们可能想要创建这个文件,而不是终止进程。Result 枚举,它定义有如下两个成员,OkErr

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

unwrap

impl<T, E: ::std::fmt::Debug> Result<T, E> {
    fn unwrap(self) -> T {
        match self {
            Result::Ok(val) => val,
            Result::Err(err) =>
              panic!("called `Result::unwrap()` on an `Err` value: {:?}", err),
        }
    }
}

TE 是泛型类型参数;现在你需要知道的就是 T代表成功时返回的 Ok 成员中的数据的类型,而 E 代表失败时返回的 Err成员中的错误的类型。因为 Result 有这些泛型类型参数,我们可以将Result 类型和标准库中为其定义的函数用于很多不同的场景,这些情况中需要返回的成功值和失败值可能会各不相同。让我们调用一个返回 Result 的函数:

use std::fs::File;
fn main() {
    let get_file_result = File::open("hello.txt");
}

File::open的返回值是 Result<T, E>。泛型参数T会被 File::open 的实现放入成功返回值的类型 std::fs::File,这是一个文件句柄。错误返回值使用的 E的类型是 std::io::Error。这些返回类型意味着 File::open 调用可能成功并返回一个可以读写的文件句柄。这个函数调用也可能会失败:例如,也许文件不存在,或者可能没有权限访问这个文件。File::open 函数需要一个方法在告诉我们成功与否的同时返回文件句柄或者错误信息。这些信息正好是 Result 枚举所代表的。

File::open 成功时,get_file_result 变量将会是一个包含文件句柄的Ok 实例。当失败时,get_file_result 变量将会是一个包含了更多关于发生了何种错误的信息的 Err 实例。

我们需要在代码中增加根据 File::open 返回值进行不同处理的逻辑:

use std::fs::File;
fn main() {
    let get_file = match File::open("hello.txt") {
        Ok(f) => f,
        Err(e) => panic!("Error: {:?}", e),
    };
}

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

这里我们告诉 Rust 当结果是 Ok 时,返回 Ok 成员中的 f 值,然后将这个文件句柄赋值给变量 get_filematch 之后,我们可以利用这个文件句柄来进行读写。

match 的另一个分支处理从 File::open 得到 Err 值的情况。在这种情况下,我们选择调用 panic! 宏。如果当前目录没有一个叫做 hello.txt 的文件,当运行这段代码时会看到如下来自 panic!宏的输出:

$ cargo run
   Compiling variables v0.1.0 (/projects/variables)
warning: unused variable: `get_file`
 --> src/main.rs:3:9
  |
3 |     let get_file = match File::open("hello.txt") {
  |         ^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_get_file`
  |
  = note: `#[warn(unused_variables)]` on by default

warning: `variables` (bin "variables") generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.73s
     Running `target/debug/variables`
thread 'main' panicked at 'Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:5:19
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

匹配不同的错误

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

use std::{fs::File, io::ErrorKind};
fn main() {
    let get_file = match File::open("hello.txt") {
        Ok(f) => f,
        Err(e) => match e.kind() {
            // 文件不存在  创建文件
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(ec) => panic!("Error ec: {:?}", ec),
            },
            // 其他错误,例如没有权限
            other_error => {
                panic!("Error  other_error: {:?}", other_error)
            }
        },
    };
}

File::open 返回的Err 成员中的值类型 io::Error,它是一个标准库中提供的结构体。这个结构体有一个返回 io::ErrorKind 值的 kind方法可供调用。io::ErrorKind 是一个标准库提供的枚举,它的成员对应 io 操作可能导致的不同错误类型。我们感兴趣的成员是 ErrorKind::NotFound,它代表尝试打开的文件并不存在。

我们希望在内层match中检查的条件是 e.kind() 的返回值是否为 ErrorKindNotFound成员。如果是,则尝试通过 File::create 创建文件。然而因为 File::create 也可能会失败,还需要增加一个内层 match 语句。当文件不能被打开,会打印出一个不同的错误信息。外层 match的最后一个分支保持不变,这样对任何除了文件不存在的错误会使程序 panic

失败时 panic 的简写:unwrapexpect

使用match 能够胜任它的工作,不过它可能有点冗长并且不总是能很好的表明其意图。 Result<T, E>类型定义了很多辅助方法来处理各种情况。其中之一叫做unwrap,它的实现就类似于 match 语句。如果 Result 值是成员Okunwrap会返回 Ok中的值。如果Result 是成员Errunwrap会为我们调用panic!

use std::fs::File;
fn main() {
    let get_file = File::open("hello.txt").unwrap();
}

如果调用这段代码时不存在 hello.txt 文件,我们将会看到一个 unwrap 调用 panic! 时提供的错误信息:

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:3:44

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

use std::fs::File;
fn main() {
    let get_file = File::open("hello.txt").expect("hello.txt 文件不存在");
}

expectunwrap的使用方式一样:返回文件句柄或调用 panic! 宏。expect 在调用 panic! 时使用的错误信息将是我们传递给 expect 的参数,而不像 unwrap那样使用默认的 panic! 信息。它看起来像这样:

thread 'main' panicked at 'hello.txt 文件不存在: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:3:44

传递错误给调用者

当编写一个其实先会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。这被称为 传播(propagating)错误,这样能更好的控制代码调用,因为比起你代码所拥有的上下文,调用者可能拥有更多信息或逻辑来决定应该如何处理错误。如果文件不存在或不能读取,这个函数会将这些错误返回给调用它的代码:

use std::fs::File;
use std::io::{Error, Read};

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

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

    let mut username = String::new();

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

fn main() {
    println!("{:?}", read_username_from_file())
}

这个函数可以编写成更加简短的形式,不过我们以大量手动处理开始以便探索错误处理;在最后我们会展示更短的形式。让我们看看函数的返回值:Result<String, Error>。这意味着函数返回一个 Result<T, E> 类型的值,其中泛型参数 T 的具体类型是 String,而 E的具体类型是 io::Error

如果这个函数没有出任何错误成功返回,函数的调用者会收到一个包含 StringOk 值 —— 函数从文件中读取到的用户名。如果函数遇到任何错误,函数的调用者会收到一个 Err 值,它储存了一个包含更多这个问题相关信息的 io::Error 实例。这里选择 io::Error 作为函数的返回值是因为它正好是函数体中那两个可能会失败的操作的错误返回值:File::open 函数和 read_to_string 方法。

函数体以调用 File::open函数开始。接着使用match 处理返回值Result,如果 File::open成功了,模式变量 file 中的文件句柄就变成了可变变量 username_file 中的值,接着函数继续执行。在 Err 的情况下,我们没有调用 panic!,而是使用 return关键字提前结束整个函数,并将来自File::open 的错误值(现在在模式变量e中)作为函数的错误值传回给调用者。

所以 username_file 中有了一个文件句柄,函数接着在变量 username中创建了一个新String并调用文件句柄 username_fileread_to_string 方法来将文件的内容读取到 username 中。read_to_string 方法也返回一个 Result 因为它也可能会失败:哪怕是 File::open 已经成功了。所以我们需要另一个 match来处理这个 Result:如果 read_to_string 成功了,那么这个函数就成功了,并返回文件中的用户名,它现在位于被封装进Okusername 中。如果read_to_string 失败了,则像之前处理 File::open 的返回值的 match 那样返回错误值。不过并不需要显式的调用 return,因为这是函数的最后一个表达式。

这种传播错误的模式在 Rust 是如此的常见,以至于Rust 提供了 ? 问号运算符来使其更易于处理。

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

read_username_from_file 函数改为使用了 ? 运算符:

use std::fs::File;
use std::io::{Error, Read};

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

    Ok(username)
}

fn main() {
    println!("{:?}", read_username_from_file())
}

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

?操作符类似于unwrap但是在遇到Error时并不会产生panic,而是会将错误返回给调用者函数。需要记住的一件事情是我们只有在函数返回OptionResult类型时才能使用?操作符。

?运算符消除了大量样板代码并使得函数的实现更简单。我们甚至可以在?之后直接使用链式方法调用来进一步缩短代码:

use std::fs::File;
use std::io::{Error, Read};

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

    Ok(username)
}

fn main() {
    println!("{:?}", read_username_from_file())
}

使用 fs::read_to_string的更为简短的写法:

use std::fs;
use std::io::Error;

fn read_username_from_file() -> Result<String, Error> {
    fs::read_to_string("hello.txt")
}

fn main() {
    println!("{:?}", read_username_from_file())
}

将文件读取到一个字符串是相当常见的操作,所以 Rust 提供了名为fs::read_to_string的函数,它会打开文件、新建一个 String、读取文件的内容,并将内容放入 String,接着返回它。

  • 原创
  • 学分: 2
  • 分类: Rust
  • 标签:
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。
127 订阅 31 篇文章

0 条评论

请先 登录 后评论
木头
木头
0xC020...10cf
江湖只有他的大名,没有他的介绍。