Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

9.2 Result枚举与可恢复的错误 Pt.1:match、expect和unwrap处理错误

9.2.1 Result枚举

通常情况下,错误都没有严重到需要停止整个程序的地步。某个函数之所以运行失败或者遇到错误,通常是由一些可以简单解释并做出响应的原因引起的。比如说程序要打开某个文件,但是这个文件并不存在;这个时候通常会考虑创建这个文件,而不是直接终止程序。

Rust提供了Result这个枚举类型来处理这种可能失败的情况。它的定义是:

#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}
}

它有两个泛型的类型参数,TE,以及两个变体,每个都关联了数据。Ok关联了TErr关联了E。泛型在10.2. 泛型会讨论,现在只需要知道:T是操作成功时Ok变体返回的数据的类型;E是操作失败时Err变体返回的错误的类型。

看个例子:

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

这段代码尝试打开一个文件,但这个文件不一定存在。换句话说,函数的运行可能会失败,所以File::open的返回值是Result枚举。这个Result里的第一个类型参数是std::fs::File,也就是成功时返回的文件类型;第二个是std::io::Error,也就是失败时返回的I/O错误。

9.2.2 用match处理Result

Option枚举一样,Result及其变体也是由预导入模块(prelude)带入作用域的,在写代码时不需要额外引入。例如:

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

如果返回的值是Ok,那么就把它所关联的值绑定到file上再返回赋给f;如果返回的值是Err,那么就把错误信息绑定在e上,由panic!宏打印出来并停止程序。

9.2.3 匹配不同的错误

我们把上面的例子完善一下。如果文件找不到,就创建这个文件。只有在连创建都不成功,或者出现了除了“找不到文件”以外的其他错误——比如没有权限打开——时,才触发panic!

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

fn main() {
    let f = File::open("6657.txt");
    let f = match f {
        Ok(file) => file,
        Err(e) => match e.kind() {
            ErrorKind::NotFound => match File::create("6657.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Problem creating file: {:?}", e),
            },
            other_error => panic!("Problem opening file: {:?}", other_error),
        },
    };
}
  • 在最外层,如果fOk,就把文件返回给f
  • 但在处理Err时有所不同。Err所附带的数据的类型是std::io::Error。这个struct上有一个.kind()方法,通过这个方法可以获得std::io::ErrorKind类型的值。它也是一个枚举,也是由标准库提供的,它里面的变体用来描述io操作可能引起的不同错误。
  • ErrorKind里面有一个变体叫ErrorKind::NotFound,表示文件不存在。这个时候就应该创建文件,下面再讨论创建文件。除了ErrorKind::NotFound,也可能有其他错误,比如说没有权限读取。这里把其他错误绑定给other_error,并由panic!打印出来然后停止程序。
  • 对于创建文件,可以使用File::create(),其参数就是文件名。而创建文件本身也有可能会失败,比如说权限不足,所以File::create()的返回值也是Result。那就再用一个match表达式来处理。如果是Ok(创建成功),那就把Ok关联的值——也就是新创建的File句柄——绑定在fc上返回赋给f;如果是Err(创建失败),就把Err所关联的错误绑定在e上,用panic!打印出来并停止程序。

match确实用得比较多,但也比较原始。这里的嵌套大大降低了可读性,虽然对比其他语言可能可读性还要高一些。而在13.1. 闭包 Pt.1:什么是闭包、如何使用闭包会讲一个概念叫做闭包(closure)。Result类型有很多方法接受闭包作为参数,而且这些方法都是使用match实现的,能够使得代码更加简洁。我把使用闭包的例子写在这里,但现在不会讲,到第13章才会讲。

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

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

9.2.4 unwrap方法

match表达式确实灵活有用,但写出来的代码也确实复杂了一些。而Result这个枚举类型本身也定义了许多辅助方法来应对各式各样的任务,其中最常用的方法之一叫unwrap

如果unwrap接收到了Ok,那么它就会把Ok附带的值返回;而如果接收到了Err,那么unwrap就会调用panic!宏。比如说,用unwrap重写9.2.2中的代码:

use std::fs::File;

fn main() {
    let f = File::open("6657.txt").unwrap();
}

unwrap本质上相当于match表达式的快捷方式。它的缺点就是错误信息不可以自定义。

9.2.5 expect方法

那如果我想要unwrap的快捷性,又想要自定义错误信息怎么办?针对这种情况,Rust提供了expect方法。如果你有印象的话,在2.1. 猜数游戏 Pt.1中就用过这种方法。

试试用expect重写unwrap的例子:

use std::fs::File;

fn main() {
    let f = File::open("6657.txt").expect("file not found");
}