错误处理
错误是软件中不可否认的事实,所以 Rust 有一些处理出错情况的特性。在许多情况下,Rust 要求你承认错误的可能性,并在你的代码编译前采取一些行动。这一要求使你的程序更加健壮,因为它可以确保你在将代码部署到生产环境之前就能发现错误并进行适当的处理。
Rust 将错误分为两大类:可恢复的(recoverable)和 不可恢复的(unrecoverable)错误。对于一个可恢复的错误,比如文件未找到的错误,我们很可能只想向用户报告问题并重试操作。不可恢复的错误总是 bug 出现的征兆,比如试图访问一个超过数组末端的位置,因此我们要立即停止程序。
大多数语言并不区分这两种错误,并采用类似异常这样方式统一处理它们。Rust 没有异常。相反,它有
Result<T, E>
类型,用于处理可恢复的错误,还有panic!
宏,在程序遇到不可恢复的错误时停止执行。
用 panic!
处理不可恢复的错误
突然有一天,代码出问题了,而你对此束手无策。对于这种情况,Rust 有
panic!
宏。在实践中有两种方法造成 panic:执行会造成代码 panic 的操作(比如访问超过数组结尾的内容)或者显式调用panic!
宏。这两种情况都会使程序 panic。通常情况下这些 panic 会打印出一个错误信息,展开并清理栈数据,然后退出。通过一个环境变量,你也可以让 Rust 在 panic 发生时打印调用堆栈(call stack)以便于定位 panic 的原因。
//调用panic!
fn main() {
panic!("crash and burn");
}
//输出
/*
PS C:\Users\pangh\Desktop\rust-project\read> $env:RUST_BACKTRACE=0 ; cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
Running `target\debug\read.exe`
thread 'main' panicked at src/main.rs:2:5:
crash and burn
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\read.exe` (exit code: 101)
*/
// 使用 panic! 的 backtrace 命令行 `$env:RUST_BACKTRACE=1 ; cargo run`
/*
当设置 RUST_BACKTRACE 环境变量时 panic! 调用所生成的 backtrace 信息
*/
//输出
/*
PS C:\Users\pangh\Desktop\rust-project\read> $env:RUST_BACKTRACE=1 ; cargo run
Compiling read v0.1.0 (C:\Users\pangh\Desktop\rust-project\read)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
Running `target\debug\read.exe`
thread 'main' panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
stack backtrace:
0: std::panicking::begin_panic_handler
at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14\library/std\src\panicking.rs:662
1: core::panicking::panic_fmt
at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14\library/core\src\panicking.rs:74
2: core::panicking::panic_bounds_check
at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14\library/core\src\panicking.rs:276
3: core::slice::index::impl$2::index<i32>
at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14\library\core\src\slice\index.rs:302
4: alloc::vec::impl$13::index<i32,usize,alloc::alloc::Global>
at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14\library\alloc\src\vec\mod.rs:2920
5: read::main
at .\src\main.rs:4
6: core::ops::function::FnOnce::call_once<void (*)(),tuple$<> >
at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14\library\core\src\ops\function.rs:250
7: core::hint::black_box
at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14\library\core\src\hint.rs:388
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
error: process didn't exit successfully: `target\debug\read.exe` (exit code: 101)
*/
用 Result
处理可恢复的错误
大部分错误并没有严重到需要程序完全停止执行。有时候,一个函数失败,仅仅就是因为一个容易理解和响应的原因。例如,如果因为打开一个并不存在的文件而失败,此时我们可能想要创建这个文件,而不是终止进程。
//Result类型
enum Result<T, E> {
Ok(T),
Err(E),
}
//使用 match 表达式处理可能会返回的 Result 成员
/*
注意与 Option 枚举一样,Result 枚举和其成员也被导入到了 prelude 中,所以就不需要在 match 分支中的 Ok 和 Err 之前指定 Result::。
这里我们告诉 Rust 当结果是 Ok 时,返回 Ok 成员中的 file 值,然后将这个文件句柄赋值给变量 greeting_file。match 之后,我们可以利用这个文件句柄来进行读写。
match 的另一个分支处理从 File::open 得到 Err 值的情况。在这种情况下,我们选择调用 panic! 宏。
*/
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => panic!("Problem opening the file: {error:?}"),
};
}
//匹配不同的错误
use std::fs::File;
use std::io::ErrorKind;
fn main() {
/*
File::open 返回的 Err 成员中的值类型 io::Error,它是一个标准库中提供的结构体。这个结构体有一个返回 io::ErrorKind 值的 kind 方法可供调用。io::ErrorKind 是一个标准库提供的枚举,它的成员对应 io 操作可能导致的不同错误类型。我们感兴趣的成员是 ErrorKind::NotFound,它代表尝试打开的文件并不存在。这样,match 就匹配完 greeting_file_result 了,不过对于 error.kind() 还有一个内层 match。
我们希望在内层 match 中检查的条件是 error.kind() 的返回值是否为 ErrorKind的 NotFound 成员。如果是,则尝试通过 File::create 创建文件。然而因为 File::create 也可能会失败,还需要增加一个内层 match 语句。当文件不能被创建,会打印出一个不同的错误信息。外层 match 的最后一个分支保持不变,这样对任何除了文件不存在的错误会使程序 panic。
*/
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
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:?}");
}
},
};
}
失败时 panic 的简写:unwrap
和 expect
match
能够胜任它的工作,不过它可能有点冗长并且不总是能很好的表明其意图。Result<T, E>
类型定义了很多辅助方法来处理各种情况。其中之一叫做unwrap
。如果Result
值是成员Ok
,unwrap
会返回Ok
中的值。如果Result
是成员Err
,unwrap
会为我们调用panic!
。
//unwrap
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt").unwrap();
}
//expect
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt").expect("找不到文件!!!!!!!!!");
}
//控制台
/*
thread 'main' panicked at src/main.rs:4:49:
找不到文件!!!!!!!!!: Os { code: 2, kind: NotFound, message: "系统找不到指定的文件。" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
*/
传播错误
当编写一个其实先会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。这被称为 传播(propagating)错误,这样能更好的控制代码调用,因为比起你代码所拥有的上下文,调用者可能拥有更多信息或逻辑来决定应该如何处理错误。
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let username_file_result = File::open("hello.txt");
let mut username_file = match username_file_result {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut username = String::new();
match username_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}
fn main() {
let user_name = read_username_from_file();
match user_name {
Ok(name) => println!("{}", name),
Err(e) => println!("{}", e),
}
}
//传播错误的简写:? 运算符
//一个使用 ? 运算符向调用者返回错误的函数
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username_file = File::open("hello.txt")?;
let mut username = String::new();
username_file.read_to_string(&mut username)?;
Ok(username)
}
//链式调用 简写
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username = String::new();
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}
//在 Option<T> 值上使用 ? 运算符
fn last_char_of_first_line(text: &str) -> Option<char> {
text.lines().next()?.chars().last()
}
fn main() {
let d = last_char_of_first_line("abcd");
println!("{:?}", d);
}
/*
可以在返回 Result 的函数中对 Result 使用 ? 运算符,可以在返回 Option 的函数中对 Option 使用 ? 运算符,但是不可以混合搭配。? 运算符不会自动将 Result 转化为 Option,反之亦然;在这些情况下,可以使用类似 Result 的 ok 方法或者 Option 的 ok_or 方法来显式转换。
*/
要不要 panic!
示例、代码原型和测试都非常适合 panic
当你编写一个示例来展示一些概念时,在拥有健壮的错误处理代码的同时也会使得例子不那么明确。例如,调用一个类似
unwrap
这样可能panic!
的方法可以被理解为一个你实际希望程序处理错误方式的占位符,它根据其余代码运行方式可能会各不相同。类似地,在我们准备好决定如何处理错误之前,
unwrap
和expect
方法在原型设计时非常方便。当我们准备好让程序更加健壮时,它们会在代码中留下清晰的标记。如果方法调用在测试中失败了,我们希望这个测试都失败,即便这个方法并不是需要测试的功能。因为
panic!
会将测试标记为失败,此时调用unwrap
或expect
是恰当的。
当我们比编译器知道更多的情况
当你有一些其他的逻辑来确保
Result
会是Ok
值时,调用unwrap
或者expect
也是合适的,虽然编译器无法理解这种逻辑。你仍然需要处理一个Result
值:即使在你的特定情况下逻辑上是不可能的,你所调用的任何操作仍然有可能失败。如果通过人工检查代码来确保永远也不会出现Err
值,那么调用unwrap
也是完全可以接受的,这里是一个例子:
/*
我们通过解析一个硬编码的字符来创建一个 IpAddr 实例。可以看出 127.0.0.1 是一个有效的 IP 地址,所以这里使用 expect 是可以接受的。然而,拥有一个硬编码的有效的字符串也不能改变 parse 方法的返回值类型:它仍然是一个 Result 值,而编译器仍然会要求我们处理这个 Result,好像还是有可能出现 Err 成员那样。这是因为编译器还没有智能到可以识别出这个字符串总是一个有效的 IP 地址。如果 IP 地址字符串来源于用户而不是硬编码进程序中的话,那么就 确实 有失败的可能性,这时就绝对需要我们以一种更健壮的方式处理 Result 了。提及这个 IP 地址是硬编码的假设会促使我们将来把 expect 替换为更好的错误处理,我们应该从其它代码获取 IP 地址。
*/
use std::net::IpAddr;
let home: IpAddr = "127.0.0.1"
.parse()
.expect("Hardcoded IP address should be valid");