Rust中的错误处理

248 阅读7分钟

今天我们来讨论两个话题。其中一个是关于处理我们在Rust开发过程中可能遇到的错误和/或失误。另一个是关于学习如何在写代码时考虑到这些问题。

目录“

  1. 错误处理
  2. 惊慌!还是不惊慌!?
  3. 结果<T,E>
  4. 解包和期望
  5. 错误传播

让我们开始学习Rust中的错误处理。

错误处理

错误会发生。这是没办法的事。即使是最好的程序员也会犯错误。你犯得越多,修正得越多,你犯的错误就会越少。但这只是趋向于0,永远不会是0。 总之,我们如何在Rust中处理错误?

Rust将错误分为两类。可恢复的错误,和不可恢复的错误。可恢复的错误是那些可以被用户快速/容易地修复的错误,并且可以再次尝试操作。不可恢复的错误通常是我们代码中的错误的症状,比如试图在数组的边界外读取。

Rust不像其他语言那样有异常。它使用返回类型Result<T, E>来处理可恢复的错误,而使用panic!宏来处理不可恢复的错误。让我们从panic宏和不可恢复的错误开始,因为这些是比较麻烦的错误。

恐慌!或不恐慌!?

正如我所说,无论如何,错误都会发生。如果错误属于不可恢复的性质,则执行panic!宏。程序在屏幕上打印出一条错误信息,然后解开(它沿着堆栈向上,清除所有分配的内存,释放给操作系统重新分配)。一旦完成了这些,它就退出了”

panic!是如何使用的?很简单!

fn main() {
    println!("I am going to panic!");
    panic!("To panic! Or not to panic! That is the question!");
}

Error-1

正如你所看到的,程序立即停止,它甚至向我们显示了它在代码中的确切位置([...] src/main.rs:3:5,第3行,我们main.rs文件的第5个字符)。

你可能会说,这一切都很好,但如果我必须执行panic!宏,它就不会捕捉到我没有想到的错误。好吧,当Rust遇到无法恢复的错误时,它会自动调用panic!宏。让我们试试在一个向量中出界,就像我在错误介绍中提到的那样“

fn main() {
    let simple_vec = vec![0, 1, 2];
    println!("{}", simple_vec[10]);
}

Error-2

一旦我们尝试这样做,Rust就会停止程序并拒绝继续。这种错误很重要,不能被忽视。(其他编程语言可能会让你在这种情况下继续下去,把责任交给作为程序员的你,让你注意到程序的怪异行为)。

如果你需要更多关于发生了什么的信息,你可以像它所说的那样,将RUST_BACKTRACE环境变量设置为1。它会喷出很多文字,关键是找到你写的代码,然后从那里开始。我不会去讨论如何做到这一点,你可以在谷歌上快速搜索如何在你的系统中做到这一点:D 继续。

结果<T, E>

幸运的是,不可恢复类型的错误并不是最常遇到的错误类型。大多数错误都很简单,可以在不完全停止程序的情况下进行修复。这种类型的错误的最简单和最明显的例子是,例如,试图打开一个不存在的文件,或者没有找到。你可以找到这个文件,或者创建一个新的文件。没有必要为此惊慌失措!让我们来试试,看看会发生什么,以及如何处理它”

use std::fs::File;

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

File中的open函数返回一个结果<T, E>。我们怎么知道?嗯,有几种方法可以知道。一个是到API文档中去查找这个函数。另一个方法(太麻烦了)是给变量f另一个类型,然后试着编译(这将会失败,因为我们说F是A类型的,而open返回的是B类型的东西)。或者,根据你选择的文本编辑器,你可以简单地在函数上悬停“

Error-3

Error-4

但它返回 Result<T, E> 是什么意思?它是什么?

结果是:

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

如果一切顺利,它返回T;如果出了问题,它返回错误E。所以,如果一切顺利,它就返回一个文件,否则,就返回Error!在这两种情况下,这只是设置变量。所以它不会自己抛出一个错误。我们必须将我们的变量与这两种可能的结果类型相匹配

use std::fs::File;

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

    match f {
        Ok(file) => file,
        Err(error) => panic!("Opening file failed: {:?}", error),
    };
}

通过这种匹配,我们"解开"我们的结果。如果没有问题,我们就把文件交给变量f。如果它包含一个错误,我们就会惊慌失措!并显示这个错误:

Error-5

( 我的操作系统是西班牙语的,所以,由于这个错误来自操作系统,它是用西班牙语写的。它基本上意味着找不到文件)

不过,我们可能想说得更具体一点。而且我们可以这样做!

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

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

    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)
            }
        },
    };
}

书中的解释是最好的,所以,让我引用它:

File::open在Err变量中返回的值的类型是io::Error,它是一个由标准库提供的结构。这个结构有一个方法kind,我们可以调用它来获得io::ErrorKind值。io::ErrorKind枚举是由标准库提供的,它的变体代表了io操作可能产生的不同种类的错误。

虽然它变得相当......繁琐。我们可以有这么多种类的错误。

有一些捷径,还有一种更好的方式来编写所有这些涉及闭包的匹配块。我将让你自己去理解闭包,因为我已经在以前的文章中解释过了!我想你会明白的。不过,捷径...有两个:Unwrap,和Expect。

解包和期望

结果类型有2个函数,可以帮助保持事情的规模,尽管并不总是建议使用它们。让我们从Unwrap开始。

如果遇到Ok()变量,Unwrap会简单地返回值,如果遇到Err()变量,则会慌乱。这将使我们的代码缩短为......(我会留下额外的代码注释,以便你能看到其中的区别)

use std::fs::File;

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

    // 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)
    //         }
    //     },
    // };
}

Error-6

正如我们在上面看到的,Unwrap给了我们默认的panic!消息。

Expect的工作原理与unwrap非常相似,但它可以让我们添加一个额外的信息,以便在它恐慌时显示。我将在结果打印中强调它:

use std::fs::File;

fn main() {
    let f = File::open("test.txt").expect("Failed to open test.txt");
}

Error-7

错误传播

这是一个方法,它允许你不在你正在编写的当前函数中处理可能的错误,而是将错误传递给这个函数的调用者并在那里处理错误。让我们快速回到之前的例子,并对其进行一些调整:

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

// Main snipped!

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

    let mut f = match f {
        Ok(file) => file,
        Err(error) => panic!("Opening file failed: {:?}", error),
    };

    let mut read_string = String::new();
    match f.read_to_string(&mut read_string) {
        Ok(_) => Ok(read_string),
        Err(e) => Err(e),
    }
}

正如你所看到的,我们可以不返回一个字符串,而是返回一个结果,让调用这个函数的人处理错误。现在,这真的很冗长,有一个快捷方式,就是 ? 操作符:

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

? 操作符放在结果后面时,其行为与匹配-> OK/Err 语句完全相同。如果是OK,它返回OK选项。如果是Err,它返回带有Err值的整个函数,就像我们放置了一个Return语句一样。整洁而方便。短暂而简明。? 操作符只能用于返回 Result、Option 或任何实现 std::ops::Try 的类型的函数。

rust中的主函数有几个有效的返回类型。其中之一是(),这也是我们到目前为止一直在使用的。或者Result<T,E> (所以你确实可以在Main里面使用?,如果你改变了返回类型的话!)

在这篇文章中,我们已经对错误处理有了初步的了解。更多信息,你可以查看书中的章节,我在下面留下了链接。

参考文献

The Rust Book中的错误处理章节