Rust 学习笔记(18)-错误处理

参考章节《Rust 程序设计语言》第9章 错误处理

Rust 将错误分为两大类:可恢复的(recoverable)和 不可恢复的(unrecoverable)错误。这一点和Go有点类似

对于一个不可恢复的错误,我们一般认为是一个bug,因此我们要立即停止程序。

一种是我们自己手动触发的painc,可以使用 painc!

1
2
3
fn main() {
    panic!("crash and burn"); // 手动触发一个painc,panic会终止程序,和golang的panic类似
}

运行程序将会出现类似这样的输出:

1
2
3
4
5
6
$ cargo run
   Compiling panic v0.1.0 (file:///projects/panic)
    Finished dev [unoptimized + debuginfo] target(s) in 0.25s
     Running `target/debug/panic`
thread 'main' panicked at 'crash and burn', src/main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

还有一种是程序 bug 引起的 painc

1
2
3
4
fn main() {
    let v = vec![1, 2, 3];
    println!("{}", v[99]);
}

小小的总结一下

  1. 说白了,painc 的目的是终止程序,
  2. painc 默认会清理堆栈,可参考书中这一小节 对应 panic 时的栈展开或终止
  3. 通过设置环境变量 RUST_BACKTRACE=1(非0就行) 打印堆栈的详细调用过程

对于一个可恢复的错误,我们很可能只想向用户报告问题并重试操作。

大部分错误并没有严重到需要程序完全停止执行。有时,一个函数可能会让我们对某种预期的错误做出反应

Rust为我们提供了一个 Result<T, E> 枚举

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

TE泛型类型参数;后面会学习到它。现在你需要知道的就是 T 代表成功时返回的 Ok 成员中的数据的类型,而 E 代表失败时返回的 Err 成员中的错误的类型。

  • 我们来看一个简单的处理 Result<T, E> 的例子
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use std::fs::File;

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

    let f = match f {
        Ok(file) => file,
        Err(error) => panic!("Problem opening the file: {:?}", error), // 发生错误时,终止程序并打印错误消息
    };
}

File::open 函数的返回值类型是 Result<T, E>
这里泛型参数 T 放入了成功值的类型 std::fs::File,它是一个文件句柄。
E 被用在失败值上时 E 的类型是 std::io::Error

  • 有时你需要更具体的错误
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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") {
                // 我们感兴趣的成员是 ErrorKind::NotFound,它代表尝试打开的文件并不存在。
                Ok(fc) => fc,
                Err(e) => panic!("Problem creating the file: {:?}", e),
            },
            other_error => {
                panic!("Problem opening the file: {:?}", other_error)
            }
        },
    };
}

io::Error 有一个返回 io::ErrorKind 值的 kind 方法可供调用。
io::ErrorKind 是一个标准库提供的枚举,它的成员对应 io 操作可能导致的不同错误类型。

我们可以通过 unwrap_or_else 方法并传递一个闭包,来稍微简化上面的写法,虽然我知道闭包是什么,但我不想在这一节写上它,因为它超出了这一节的范畴

  • 失败时 panic 的简写:unwrapexpect

如果 Result 值是成员 Ok,unwrap 会返回 Ok 中的值。如果 Result 是成员 Err,unwrap 会为我们调用 panic!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use std::fs::File;

fn main() {
    let f = File::open("text.txt");
    let f = match f {
        Ok(file) => file,
        Err(error) => panic!("Problem opening the file: {:?}", error), // 发生错误时,终止程序并打印错误消息
    };

    // 上面的可以简写成下面的格式
    File::open("text.txt").unwrap();

    // 还有另一个类似于 unwrap 的方法它还允许我们选择 panic! 的错误信息:
    File::open("text.txt").expect("Problem opening the file");
}
  • 接下来我们来看一下,我们自己如何返回一个 Result

我们在编写函数或方法时,我们明确知道,我们这个函数或方法可能会发生一个错误,而这个错误是调用者可以处理的,它不是一个无法处理的错误
那么我们就可以让函数或方法返回 Result 类型,这样调用者就知道,你这个函数或方法可能会发生错误,需要他来处理

第一种方法,最苯的方法,不推荐 通过 match 关键字匹配

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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), // 失败返回io::Error,可以通过 return语句 提前返回
    };

    let mut s = String::new();

    match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        Err(e) => Err(e), // 失败返回io::Error
    }
}

第二种方法,? 号元算符简化 需要注意的是 ? 运算符只能在 返回Result 的函数中使用

1
2
3
4
5
6
7
8
fn read_username_from_file() -> Result<String, io::Error> {
    // File::open 调用结尾的 ? 将会把 Ok 中的值返回给变量 f。
    // 如果出现了错误,? 运算符会提早返回整个函数并将一些 Err 值传播给调用者。
    let mut f = File::open("hello.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?; // 同理也适用于 read_to_string 调用结尾的 ?。
    Ok(s) // 把s包装到Ok成员中返回,因为返回值类型是 Result,这句话要是看不懂的话,你可以去面壁思过了
}

上面的例子,当 ? 号运算符所在的语句发生错误时,Err 中的值将作为整个函数的返回值,就好像使用了 return 关键字一样,这样错误值就被传播给了调用者。

还可以再简化一下

1
2
3
4
5
6
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)
}

总结一下

  1. painc 代表不可恢复错误,这通常是程序发生了bug, 发生 painc 时程序会终止,通过 painc! 宏可以手动触发 painc
  2. Result<T, E> 代表可恢复的错误,其中 T 代表成功时返回的 Ok 成员中的数据的类型,而 E 代表失败时返回的 Err 成员中的错误的类型。
  3. io::Error 有一个返回 io::ErrorKind 值的 kind 方法可供调用,为我们提供了获取更具体的错误类型的能力
  4. unwrap 如果 Result 值是成员 Okunwrap 会返回 Ok 中的值。如果 Result 是成员 Errunwrap 会为我们调用 panic!
  5. expectunwrap 功能一样,只不过 expect 可以提供自定义信息
  6. ? 号运算符可以简化代码,? 号运算符只能在 返回Result 的函数中使用
  7. ? 号运算符可以链式调用
updatedupdated2025-03-012025-03-01