可恢复和不可恢复的错误
Rust
将错误分为两大类:可恢复的(recoverable)
和 不可恢复的(unrecoverable)
错误。对于一个可恢复的错误,比如文件未找到的错误,我们很可能只想向用户报告问题并重试操作。不可恢复的错误总是 bug
出现的征兆,比如试图访问一个超过数组末端的位置,因此我们要立即停止程序。
大多数语言并不区分这两种错误,并采用类似异常这样方式统一处理他们。Rust
没有异常。相反,它有Result<T, E>
类型,用于处理可恢复的错误,还有 panic!
宏,在程序遇到不可恢复的错误时停止执行。
panic!
不可恢复的错误突然有一天,代码出问题了,而你对此束手无策。对于这种情况,Rust
有 panic!
宏。在实践中有两种方法造成 panic
:执行会造成代码 panic
的操作(比如访问超过数组结尾的内容)或者显式调用panic!
宏。这两种情况都会使程序 panic
。通常情况下这些 panic
会打印出一个错误信息,展开并清理栈数据,然后退出。
fn guess(n: i32) -> bool {
if n < 1 || n > 10 {
panic!("Invalid number: {}", n);
}
n == 5
}
fn main() {
guess(11);
}
错误:
$ cargo run
Compiling variables v0.1.0 (/projects/variables)
Finished dev [unoptimized + debuginfo] target(s) in 0.63s
Running `target/debug/variables`
thread 'main' panicked at 'Invalid number: 11', src/main.rs:3:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
panic
会导致当前线程结束,甚至是整个程序的结束,这往往是不被期望看到的结果。当然当程序遇到了致命的bug
,panic
是最优选择。
Result
可恢复的错误大部分错误并没有严重到需要程序完全停止执行。有时,一个函数会因为一个容易理解并做出反应的原因失败。例如,如果因为打开一个并不存在的文件而失败,此时我们可能想要创建这个文件,而不是终止进程。Result
枚举,它定义有如下两个成员,Ok
和 Err
:
enum Result<T, E> {
Ok(T),
Err(E),
}
unwrap
impl<T, E: ::std::fmt::Debug> Result<T, E> {
fn unwrap(self) -> T {
match self {
Result::Ok(val) => val,
Result::Err(err) =>
panic!("called `Result::unwrap()` on an `Err` value: {:?}", err),
}
}
}
T
和 E
是泛型类型参数;现在你需要知道的就是 T
代表成功时返回的 Ok
成员中的数据的类型,而 E
代表失败时返回的 Err
成员中的错误的类型。因为 Result
有这些泛型类型参数,我们可以将Result
类型和标准库中为其定义的函数用于很多不同的场景,这些情况中需要返回的成功值和失败值可能会各不相同。让我们调用一个返回 Result
的函数:
use std::fs::File;
fn main() {
let get_file_result = File::open("hello.txt");
}
File::open
的返回值是 Result<T, E>
。泛型参数T
会被 File::ope
n 的实现放入成功返回值的类型 std::fs::File
,这是一个文件句柄。错误返回值使用的 E
的类型是 std::io::Error
。这些返回类型意味着 File::open
调用可能成功并返回一个可以读写的文件句柄。这个函数调用也可能会失败:例如,也许文件不存在,或者可能没有权限访问这个文件。File::open
函数需要一个方法在告诉我们成功与否的同时返回文件句柄或者错误信息。这些信息正好是 Result
枚举所代表的。
当 File::open
成功时,get_file_result
变量将会是一个包含文件句柄的Ok
实例。当失败时,get_file_result
变量将会是一个包含了更多关于发生了何种错误的信息的 Err
实例。
我们需要在代码中增加根据 File::open
返回值进行不同处理的逻辑:
use std::fs::File;
fn main() {
let get_file = match File::open("hello.txt") {
Ok(f) => f,
Err(e) => panic!("Error: {:?}", e),
};
}
注意与 Option
枚举一样,Result
枚举和其成员也被导入到了 prelude
中,所以就不需要在 match
分支中的 Ok
和 Err
之前指定Result::
。
这里我们告诉 Rust
当结果是 Ok
时,返回 Ok
成员中的 f
值,然后将这个文件句柄赋值给变量 get_file
。match
之后,我们可以利用这个文件句柄来进行读写。
match
的另一个分支处理从 File::open
得到 Err
值的情况。在这种情况下,我们选择调用 panic!
宏。如果当前目录没有一个叫做 hello.txt
的文件,当运行这段代码时会看到如下来自 panic!
宏的输出:
$ cargo run
Compiling variables v0.1.0 (/projects/variables)
warning: unused variable: `get_file`
--> src/main.rs:3:9
|
3 | let get_file = match File::open("hello.txt") {
| ^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_get_file`
|
= note: `#[warn(unused_variables)]` on by default
warning: `variables` (bin "variables") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/variables`
thread 'main' panicked at 'Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:5:19
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
上面代码不管 File::open
是因为什么原因失败都会 panic!
。我们真正希望的是对不同的错误原因采取不同的行为:如果 File::open
因为文件不存在而失败,我们希望创建这个文件并返回新文件的句柄。如果 File::open
因为任何其他原因失败,例如没有打开文件的权限,我们仍然希望像上面那样 panic!
:
use std::{fs::File, io::ErrorKind};
fn main() {
let get_file = match File::open("hello.txt") {
Ok(f) => f,
Err(e) => match e.kind() {
// 文件不存在 创建文件
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(ec) => panic!("Error ec: {:?}", ec),
},
// 其他错误,例如没有权限
other_error => {
panic!("Error other_error: {:?}", other_error)
}
},
};
}
File::open
返回的Err
成员中的值类型 io::Error
,它是一个标准库中提供的结构体。这个结构体有一个返回 io::ErrorKind
值的 kind
方法可供调用。io::ErrorKind
是一个标准库提供的枚举,它的成员对应 io
操作可能导致的不同错误类型。我们感兴趣的成员是 ErrorKind::NotFound
,它代表尝试打开的文件并不存在。
我们希望在内层match
中检查的条件是 e.kind()
的返回值是否为 ErrorKind
的 NotFound
成员。如果是,则尝试通过 File::create
创建文件。然而因为 File::create
也可能会失败,还需要增加一个内层 match
语句。当文件不能被打开,会打印出一个不同的错误信息。外层 match
的最后一个分支保持不变,这样对任何除了文件不存在的错误会使程序 panic
。
panic
的简写:unwrap
和expect
使用match
能够胜任它的工作,不过它可能有点冗长并且不总是能很好的表明其意图。 Result<T, E>
类型定义了很多辅助方法来处理各种情况。其中之一叫做unwrap
,它的实现就类似于 match
语句。如果 Result
值是成员Ok
,unwrap
会返回 Ok
中的值。如果Result
是成员Err
,unwrap
会为我们调用panic!
:
use std::fs::File;
fn main() {
let get_file = File::open("hello.txt").unwrap();
}
如果调用这段代码时不存在 hello.txt
文件,我们将会看到一个 unwrap
调用 panic!
时提供的错误信息:
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:3:44
还有另一个类似于 unwrap
的方法它还允许我们选择panic!
的错误信息:expect
。使用 expect
而不是 unwrap
并提供一个好的错误信息可以表明你的意图并更易于追踪 panic
的根源。expect
的语法看起来像这样:
use std::fs::File;
fn main() {
let get_file = File::open("hello.txt").expect("hello.txt 文件不存在");
}
expect
与unwrap
的使用方式一样:返回文件句柄或调用 panic!
宏。expect
在调用 panic!
时使用的错误信息将是我们传递给 expect
的参数,而不像 unwrap
那样使用默认的 panic!
信息。它看起来像这样:
thread 'main' panicked at 'hello.txt 文件不存在: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:3:44
当编写一个其实先会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。这被称为 传播(propagating)
错误,这样能更好的控制代码调用,因为比起你代码所拥有的上下文,调用者可能拥有更多信息或逻辑来决定应该如何处理错误。如果文件不存在或不能读取,这个函数会将这些错误返回给调用它的代码:
use std::fs::File;
use std::io::{Error, Read};
fn read_username_from_file() -> Result<String, Error> {
let username_file_result = File::open("hello.txt");
let mut username_file = match username_file_result {
Ok(f) => f,
Err(e) => return Err(e),
};
let mut username = String::new();
match username_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => return Err(e),
}
}
fn main() {
println!("{:?}", read_username_from_file())
}
这个函数可以编写成更加简短的形式,不过我们以大量手动处理开始以便探索错误处理;在最后我们会展示更短的形式。让我们看看函数的返回值:Result<String, 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
,如果 File::open
成功了,模式变量 file
中的文件句柄就变成了可变变量 username_file
中的值,接着函数继续执行。在 Err
的情况下,我们没有调用 panic!
,而是使用 return
关键字提前结束整个函数,并将来自File::open
的错误值(现在在模式变量e
中)作为函数的错误值传回给调用者。
所以 username_file
中有了一个文件句柄,函数接着在变量 username
中创建了一个新String
并调用文件句柄 username_file
的 read_to_string
方法来将文件的内容读取到 username
中。read_to_string
方法也返回一个 Result
因为它也可能会失败:哪怕是 File::open
已经成功了。所以我们需要另一个 match
来处理这个 Result
:如果 read_to_strin
g 成功了,那么这个函数就成功了,并返回文件中的用户名,它现在位于被封装进Ok
的 username
中。如果read_to_string
失败了,则像之前处理 File::open
的返回值的 match
那样返回错误值。不过并不需要显式的调用 return
,因为这是函数的最后一个表达式。
这种传播错误的模式在 Rust 是如此的常见,以至于Rust
提供了 ?
问号运算符来使其更易于处理。
?
运算符read_username_from_file
函数改为使用了 ?
运算符:
use std::fs::File;
use std::io::{Error, Read};
fn read_username_from_file() -> Result<String, Error> {
let mut username_file = File::open("hello.txt")?;
let mut username = String::new();
username_file.read_to_string(&mut username)?;
Ok(username)
}
fn main() {
println!("{:?}", read_username_from_file())
}
Result
值之后的 ?
被定义与上面示例中定义的处理 Result
值的 match
表达式有着完全相同的工作方式。如果 Result
的值是 Ok
,这个表达式将会返回 Ok
中的值而程序将继续执行。如果值是 Err
,Err
中的值将作为整个函数的返回值,就好像使用了 return
关键字一样,这样错误值就被传播给了调用者。
?
操作符类似于unwrap
但是在遇到Error
时并不会产生panic
,而是会将错误返回给调用者函数。需要记住的一件事情是我们只有在函数返回Option
和Result
类型时才能使用?
操作符。
?
运算符消除了大量样板代码并使得函数的实现更简单。我们甚至可以在?
之后直接使用链式方法调用来进一步缩短代码:
use std::fs::File;
use std::io::{Error, Read};
fn read_username_from_file() -> Result<String, Error> {
let mut username = String::new();
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}
fn main() {
println!("{:?}", read_username_from_file())
}
使用 fs::read_to_string
的更为简短的写法:
use std::fs;
use std::io::Error;
fn read_username_from_file() -> Result<String, Error> {
fs::read_to_string("hello.txt")
}
fn main() {
println!("{:?}", read_username_from_file())
}
将文件读取到一个字符串是相当常见的操作,所以 Rust
提供了名为fs::read_to_string
的函数,它会打开文件、新建一个 String
、读取文件的内容,并将内容放入 String
,接着返回它。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!