Result
与可恢复的错误
ch09-01-unrecoverable-errors-with-panic.md
commit 0c1d55ef48e5f6cf6a3b221f5b6dd4c922130bb1
大部分错误并没有严重到需要程序完全停止执行。有时,一个函数会因为一个容易理解并回应的原因失败。例如,如果尝试打开一个文件不过由于文件并不存在而操作就失败,这是我们可能想要创建这个文件而不是终止进程。
回忆一下第二章“使用Result
类型来处理潜在的错误”部分中的那个Result
枚举,它定义有如下连个成员,Ok
和Err
:
enum Result<T, E> {
Ok(T),
Err(E),
}
T
和E
是泛型类型参数;第十章会详细介绍泛型。现在你需要知道的就是T
代表成功时返回的Ok
成员中的数据的类型,而E
代表失败时返回的Err
成员中的错误的类型。因为Result
有这些泛型类型参数,我们可以将Result
类型和标准库中为其定义的函数用于很多不同的场景,这些情况中需要返回的成功值和失败值可能会各不相同。
让我们调用一个返回Result
的函数,因为它可能会失败:如列表 9-2 所示打开一个文件:
如何知道File::open
返回一个Result
呢?我们可以查看标准库 API 文档,或者可以直接问编译器!如果给f
某个我们知道不是函数返回值类型的类型注解,接着尝试编译代码,编译器会告诉我们类型不匹配。然后错误信息会告诉我们f
的类型应该是什么,为此我们将let f
语句改为:
let f: u32 = File::open("hello.txt");
现在尝试编译会给出如下错误:
error[E0308]: mismatched types
--> src/main.rs:4:18
|
4 | let f: u32 = File::open("hello.txt");
| ^^^^^^^^^^^^^^^^^^^^^^^ expected u32, found enum
`std::result::Result`
|
= note: expected type `u32`
= note: found type `std::result::Result<std::fs::File, std::io::Error>`
这就告诉我们了File::open
函数的返回值类型是Result<T, E>
。这里泛型参数T
放入了成功值的类型std::fs::File
,它是一个文件句柄。E
被用在失败值上其类型是std::io::Error
。
这个返回值类型说明File::open
调用可能会成功并返回一个可以进行读写的文件句柄。这个函数也可能会失败:例如,文件可能并不存在,或者可能没有访问文件的权限。File::open
需要一个方式告诉我们是成功还是失败,并同时提供给我们文件句柄或错误信息。而这些信息正是Result
枚举可以提供的。
当File::open
成功的情况下,变量f
的值将会是一个包含文件句柄的Ok
实例。在失败的情况下,f
会是一个包含更多关于出现了何种错误信息的Err
实例。
我们需要在列表 9-2 的代码中增加根据File::open
返回值进行不同处理的逻辑。列表 9-3 展示了一个处理Result
的基本工具:第六章学习过的match
表达式。
注意与Option
枚举一样,Result
枚举和其成员也被导入到了 prelude 中,所以就不需要在match
分支中的Ok
和Err
之前指定Result::
。
这里我们告诉 Rust 当结果是Ok
,返回Ok
成员中的file
值,然后将这个文件句柄赋值给变量f
。match
之后,我们可以利用这个文件句柄来进行读写。
match
的另一个分支处理从File::open
得到Err
值的情况。在这种情况下,我们选择调用panic!
宏。如果当前目录没有一个叫做 hello.txt 的文件,当运行这段代码时会看到如下来自panic!
宏的输出:
thread 'main' panicked at 'There was a problem opening the file: Error { repr:
Os { code: 2, message: "No such file or directory" } }', src/main.rs:8
匹配不同的错误
列表 9-3 中的代码不管File::open
是因为什么原因失败都会panic!
。我们真正希望的是对不同的错误原因采取不同的行为:如果File::open
因为文件不存在而失败,我们希望创建这个文件并返回新文件的句柄。如果File::open
因为任何其他原因失败,例如没有打开文件的权限,我们仍然希望像列表 9-3 那样panic!
。让我们看看列表 9-4,其中match
增加了另一个分支:
File::open
返回的Err
成员中的值类型io::Error
,它是一个标准库中提供的结构体。这个结构体有一个返回io::ErrorKind
值的kind
方法可供调用。io::ErrorKind
是一个标准库提供的枚举,它的成员对应io
操作可能导致的不同错误类型。我们感兴趣的成员是ErrorKind::NotFound
,它代表尝试打开的文件并不存在。
if error.kind() == ErrorKind::NotFound
条件被称作 match guard:它是一个进一步完善match
分支模式的额外的条件。这个条件必须为真才能使分支的代码被执行;否则,模式匹配会继续并考虑match
中的下一个分支。模式中的ref
是必须的,这样error
就不会被移动到 guard 条件中而只是仅仅引用它。第十八章会详细解释为什么在模式中使用ref
而不是&
来获取一个引用。简而言之,在模式的上下文中,&
匹配一个引用并返回它的值,而ref
匹配一个值并返回一个引用。
在 match guard 中我们想要检查的条件是error.kind()
是否是ErrorKind
枚举的NotFound
成员。如果是,尝试用File::create
创建文件。然而File::create
也可能会失败,我们还需要增加一个内部match
语句。当文件不能被打开,会打印出一个不同的错误信息。外部match
的最后一个分支保持不变这样对任何除了文件不存在的错误会使程序 panic。
失败时 panic 的捷径:unwrap
和expect
match
能够胜任它的工作,不过它可能有点冗长并且并不总是能很好的表明意图。Result<T, E>
类型定义了很多辅助方法来处理各种情况。其中之一叫做unwrap
,它的实现就类似于列表 9-3 中的match
语句。如果Result
值是成员Ok
,unwrap
会返回Ok
中的值。如果Result
是成员Err
,unwrap
会为我们调用panic!
。
use std::fs::File;
fn main() {
let f = File::open("hello.txt").unwrap();
}
如果调用这段代码时不存在 hello.txt 文件,我们将会看到一个unwrap
调用panic!
时提供的错误信息:
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error {
repr: Os { code: 2, message: "No such file or directory" } }',
/stable-dist-rustc/build/src/libcore/result.rs:868
还有另一个类似于unwrap
的方法它还允许我们选择panic!
的错误信息:expect
。使用expect
而不是unwrap
并提供一个好的错误信息可以表明你的意图并有助于追踪 panic 的根源。expect
的语法看起来像这样:
use std::fs::File;
fn main() {
let f = File::open("hello.txt").expect("Failed to open hello.txt");
}
expect
与unwrap
的使用方式一样:返回文件句柄或调用panic!
宏。expect
用来调用panic!
的错误信息将会作为传递给expect
的参数,而不像unwrap
那样使用默认的panic!
信息。它看起来像这样:
thread 'main' panicked at 'Failed to open hello.txt: Error { repr: Os { code:
2, message: "No such file or directory" } }',
/stable-dist-rustc/build/src/libcore/result.rs:868
传播错误
当编写一个其实现会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。这被称为传播(propagating)错误,这样能更好的控制代码调用,因为比起你代码所拥有的上下文,调用者可能拥有更多信息或逻辑来决定应该如何处理错误。
例如,列表 9-5 展示了一个从文件中读取用户名的函数。如果文件不存在或不能读取,这个函数会将这些错误返回给调用它的代码:
首先让我们看看函数的返回值:Result<String, io::Error>
。这意味着函数返回一个Result<T, E>
类型的值,其中泛型参数T
的具体类型是String
,而E
的具体类型是io::Error
。如果这个函数没有出任何错误成功返回,函数的调用者会收到一个包含String
的Ok
值————函数从文件中读取到的用户名。如果函数遇到任何错误,函数的调用者会收到一个Err
值,它储存了一个包含更多这个问题相关信息的io::Error
实例。我们选择io::Error
作为函数的返回值是因为它正好是函数体中那两个可能会失败的操作的错误返回值:File::open
函数和read_to_string
方法。
函数体以File::open
函数开头。接着使用match
处理返回值Result
,类似于列表 9-3 中的match
,唯一的区别是不再当Err
时调用panic!
,而是提早返回并将File::open
返回的错误值作为函数的错误返回值传递给调用者。如果File::open
成功了,我们将文件句柄储存在变量f
中并继续。
接着我们在变量s
中创建了一个新String
并调用文件句柄f
的read_to_string
方法来将文件的内容读取到s
中。read_to_string
方法也返回一个Result
因为它也可能会失败:哪怕是File::open
已经成功了。所以我们需要另一个match
来处理这个Result
:如果read_to_string
成功了,那么这个函数就成功了,并返回文件中的用户名,它现在位于被封装进Ok
的s
中。如果read_to_string
失败了,则像之前处理File::open
的返回值的match
那样返回错误值。并不需要显式的调用return
,因为这是函数的最后一个表达式。
调用这个函数的代码最终会得到一个包含用户名的Ok
值,亦或一个包含io::Error
的Err
值。我们无从得知调用者会如何处理这些值。例如,如果他们得到了一个Err
值,他们可能会选择panic!
并使程序崩溃、使用一个默认的用户名或者从文件之外的地方寻找用户名。我们没有足够的信息知晓调用者具体会如何尝试,所以将所有的成功或失败信息向上传播,让他们选择合适处理方法。
这种传播错误的模式在 Rust 是如此的常见,以至于有一个更简便的专用语法:?
。
传播错误的捷径:?
列表 9-6 展示了一个read_username_from_file
的实现,它实现了与列表 9-5 中的代码相同的功能,不过这个实现是使用了问号运算符:
Result
值之后的?
被定义为与列表 9-5 中定义的处理Result
值的match
表达式有着完全相同的工作方式。如果Result
的值是Ok
,这个表达式将会返回Ok
中的值而程序将继续执行。如果值是Err
,Err
中的值将作为整个函数的返回值,就好像使用了return
关键字一样,这样错误值就被传播给了调用者。
在列表 9-6 的上下文中,File::open
调用结尾的?
将会把Ok
中的值返回给变量f
。如果出现了错误,?
会提早返回整个函数并将任何Err
值传播给调用者。同理也适用于read_to_string
调用结尾的?
。
?
消除了大量样板代码并使得函数的实现更简单。我们甚至可以在?
之后直接使用链式方法调用来进一步缩短代码:
use std::io;
use std::io::Read;
use std::fs::File;
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)
}
在s
中创建新的String
被放到了函数开头;这没有什么变化。我们对File::open("hello.txt")?
的结果直接链式调用了read_to_string
,而不再创建变量f
。仍然需要read_to_string
调用结尾的?
,而且当File::open
和read_to_string
都成功没有失败时返回包含用户名s
的Ok
值。其功能再一次与列表 9-5 和列表 9-5 保持一致,不过这是一个与众不同且更符合工程学的写法。
?
只能被用于返回Result
的函数
?
只能被用于返回值类型为Result
的函数,因为他被定义为与列表 9-5 中的match
表达式有着完全相同的工作方式。match
的return Err(e)
部分要求返回值类型是Result
,所以函数的返回值必须是Result
才能与这个return
相兼容。
让我们看看在main
函数中使用?
会发生什么,如果你还记得的话它的返回值类型是()
:
use std::fs::File;
fn main() {
let f = File::open("hello.txt")?;
}