今天我们来讨论两个话题。其中一个是关于处理我们在Rust开发过程中可能遇到的错误和/或失误。另一个是关于学习如何在写代码时考虑到这些问题。
目录“
- 错误处理
- 惊慌!还是不惊慌!?
- 结果<T,E>
- 解包和期望
- 错误传播
让我们开始学习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!");
}
正如你所看到的,程序立即停止,它甚至向我们显示了它在代码中的确切位置([...] 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]);
}
一旦我们尝试这样做,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类型的东西)。或者,根据你选择的文本编辑器,你可以简单地在函数上悬停“
但它返回 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。如果它包含一个错误,我们就会惊慌失措!并显示这个错误:
( 我的操作系统是西班牙语的,所以,由于这个错误来自操作系统,它是用西班牙语写的。它基本上意味着找不到文件)
不过,我们可能想说得更具体一点。而且我们可以这样做!
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)
// }
// },
// };
}
正如我们在上面看到的,Unwrap给了我们默认的panic!消息。
Expect的工作原理与unwrap非常相似,但它可以让我们添加一个额外的信息,以便在它恐慌时显示。我将在结果打印中强调它:
use std::fs::File;
fn main() {
let f = File::open("test.txt").expect("Failed to open test.txt");
}
错误传播
这是一个方法,它允许你不在你正在编写的当前函数中处理可能的错误,而是将错误传递给这个函数的调用者并在那里处理错误。让我们快速回到之前的例子,并对其进行一些调整:
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里面使用?,如果你改变了返回类型的话!)
在这篇文章中,我们已经对错误处理有了初步的了解。更多信息,你可以查看书中的章节,我在下面留下了链接。