在Rust中,将错误分为两种,可恢复错误和不可恢复错误。所谓可恢复错误就是指类似于文件未找到这类错误,一般需要将它们报告给用户并再次尝试进行操作,而不可恢复错误往往就是Bug,需要停止程序的运行。

1、不可恢复错误与panic!:

当代码中出现没有预料到的错误时,Rust提供了一个特殊的宏panoc!,程序会在panic!宏执行时打印一段错误提示信息,展开并清理当前的调用栈,然后退出程序的执行。
当panic发生时,程序会默认开始栈展开。这意味着Rust会沿着调用栈的反向顺序遍历所有调用函数,并依次清理这些函数中的数据。但是为了支持这种遍历和清理操作,我们需要在二进制中存储许多额外信息。

2、可恢复错误与Result:

在程序的调试中,有些错误没有严重到需要停止整个程序的运行,例如尝试打开一个文件,而文件不存在的情况。这种情况可以使用Result类型来处理。在Result枚举中,定义了两个变体——OK和Err。示例:

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

这里的T和E都是泛型,T代表了OK变体中包含的值的类型,该变体中的值会在执行成功时返回;E代表了Err变体中包含的错误类型,该变体会在执行失败时返回。

(1)、匹配不同的错误:

由于在编程时,会遇到不同的错误,那么就可以根据错误的种类来执行不同的操作。示例:

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") {Ok(fc) => fc,Err(e) => panic!("Tried to create file but there was a problem:{:?}", e), },other_error => panic!("There was a problem opening the file: {:?}",other_error), }, };}

File::open返回的Err变体中的错误值类型,是定义在某个标准库中的结构体类型:io::Error。这个结构体拥有一个被称作kind的方法,可以通过调用它来获得 io::ErrorKind 值。这个io::ErrorKind枚举是由标准库提供的,它的变体被用于描述io操作所可能导致的不同错误。这里使用的变体是ErrorKind::NotFound,它用于说明我们尝试打开的文件不存在。所以,我们不但对变量f使用了match表达式,还在内部对error.kind()使用了match表达式。

(2)、失败时触发panic的快捷方式:unwrap和expect:

虽然使用match运行得很不错,但使用它所编写出来的代码可能会显得有些冗长,且无法较好地表明其意图。类型Result本身也定义了许多辅助方法来应对各式各样的任务。当Result的返回值是Ok变体时,unwrap就会返回Ok内部的值。而当Result的返回值是Err变体时,unwrap则会替我们调用panic! 宏。示例:

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

还有另外一个被称作expect的方法,它允许我们在unwrap的基础上指定panic! 所附带的错误提示信息。使用expect并附带上一段清晰的错误提示信息可以阐明你的意图,并使你更容易追踪到panic的起源。示例:

use std::fs::File;fn main() {let f = File::open("hello.txt").expect("Failed to open hello.txt");}

使用expect所实现的功能与unwrap完全一样:要么返回指定文件句柄,要么触发panic! 宏调用。唯一的区别在于,expect触发panic! 时会将传入的参数字符串作为错误提示信息输出,而unwrap触发的panic! 则只会携带一段简短的默认信息。

(3)、传播错误:

编写的函数中包含了一些可能会执行失败的调用时,除了可以在函数中处理这个错误,还可以将这个错误返回给调用者,让他们决定应该如何做进一步处理。这个过程也被称作传播错误,在调用代码时它给了用户更多的控制能力。与编写代码时的上下文环境相比,调用者可能会拥有更多的信息和逻辑来决定应该如何处理错误。
传播错误的模式在Rust编程中非常常见,所以Rust专门提供了一个问号运算符(?)来简化它的语法。示例:

use std::io;use std::io::Read;use std::fs::File;fn read_username_from_file() -> Result<String, io::Error> {let mut f = File::open("hello.txt")?;let mut s = String::new();f.read_to_string(&mut s)?;Ok(s)}

通过将放置于Result值之后,我们实现了与使用match表达式来处理Result时一样的功能。假如这个Result的值是Ok,那么包含在Ok中的值就会作为这个表达式的结果返回并继续执行程序。假如值是Err,那么这个值就会作为整个程序的结果返回,如同使用了return一样将错误传播给调用者。
match表达式与运算符的一个区别:被运算符所接收的错误值会隐式地被from函数处理,这个函数定义于标准库的From trait中,用于在错误类型之间进行转换。当运算符调用from函数时,它就开始尝试将传入的错误类型转换为当前函数的返回错误类型。当一个函数拥有不同的失败原因,却使用了统一的错误返回类型来同时进行表达时,这个功能会十分有用。只要每个错误类型都实现了转换为返回错误类型的from函数,?运算符就会自动处理所有的转换过程。
注:?运算符只能被用于返回Result的函数。

3、要不要使用panic!:

什么时候应该使用panic!,而什么时候又应该返回Result呢?代码一旦发生panic,就再也没有恢复的可能了。只要你认为自己可以代替调用者决定某种情形是不可恢复的,那么就可以使用panic!,而不用考虑错误是否存在可以恢复的机会。当你选择返回一个Result值时,你就将这种选择权交给了调用者。调用者可以根据自己的实际情况来决定是否要尝试进行恢复,或者干脆认为Err是不可恢复的,并使用panic! 来将可恢复错误转变为不可恢复错误。因此,我们会在定义一个可能失败的函数时优先考虑使用Result方案。