本教程环境
系统:MacOS
Rust 版本:1.77.2
错误在程序开发中是不可避免的。Rust 将错误分为两大类:可恢复的 和不可恢复的。 发生不可恢复的错误,例如数组越界,程序会 panic
。 同时,Rust 提供了 Result<T, E>
类型来处理可恢复的错误,例如文件读取错误。
panic
用来处理永远不应该发生的错误。例如:
- 数组越界访问;
- 整数除以 0;
- 在恰好为 Err 的 Result 上调用
.expect()
; - 断言失败。
当代码检测到出现错误需要立即触发 panic 时,可以使用 panic!()
宏。 Rust 在发生 panic 时可以展开调用栈(默认),也可中止进程。
展开调用栈
例如当整数除以 0 的时候会触发 panic。
rust
fn main() {
println!("{}", 100/0);
}
报错如下: 在 Rust 中触发了 panic,通常会按如下方式处理:
- 打印一条错误信息到终端。
- 展开调用栈;
- 最后,线程退出。如果 panic 是主线程,退出整个进程。
panic 是基于线程的。一个线程 panic,其他线程可以继续做自己的事。 还有一种方式可以捕获调用栈展开,让线程存活并继续运行。std::panic::catch_unwind()
可以做到这一点。这是 Rust 的测试工具用于在测试中断言失败时进行恢复的机制。
中止
展开调用栈事默认的 panic 行为,但是,在两种情况下 Rust 不会试图展开调用栈。 Rust 在试图处理第一个 panic 时,.drop()
方法触发了第二个 panic,那么这个 panic 就是致命的。Rust 会停止展开调用栈并中止整个进程。 此外,Rust 处理 panic 的行为是可定制的。如果使用 -C panic=abort
参数进行编译,那么程序中的第一个 panic 会立即中止进程。
Result
Rust 中没有异常。函数执行失败时候会返回 Result
类型。它会指示出可能的失败。要么返回一个成功的结果,要么返回一个错误的结果。
rust
fn get_weather(location: LatLng) -> Result<WeatherReport, io::Error>
捕获错误
可使用 match
来处理 Result
。
rust
match get_weather(hometown) {
Ok(report) => {
display_weather(hometown, &report);
}
Err(err) => {
println!("error querying the weather: {}", err);
shedule_weather_retry();
}
}
match
有点冗长,所以针对一些常见场景提供了多个有用方法。
result.is_ok()
已成功result.is_err()
已出错result.ok()
成功值result.err()
错误值result.unwarp_or(fallback)
如果 result 为成功结果,就返回成功值;否则,返回 fallback,丢弃错误值。result.unwarp_or_else(fallbakc_fn)
解包,否则调用result.unwarp()
解包。如果 result 错误,发生 panic。result.expect(message)
与unwarp()
相同,但是可以提供在 panic 时的消息。result.as_ref()
转引用,将Result<T, E>
转为Result<&T, &E>
。result.as_mut()
转可变引用。
Result 类型别名
如果 Rust 文档中忽略了 Result 中的错误类型:
rust
fn remove_file(path: &Path) -> Result<()>
这意味着使用了 Result 的类型别名。 例如,标准库的 std::io
模块定义了如下的别名。
rust
pub type Result<T> = result::Result<T, Error>;
打印错误
标准库中定义了几种错误类型。
std::io::Error
;std::fmt::Error
;std:::str::Utf8Error
它们都实现了 std::error::Error
特型,意味着它们都有以下特性和方法:
- 可通过
println!()
进行打印。使用格式说明符{}
打印简短错误信息。或使用{:?}
获取该错误的Debug
视图。 err.to_string()
转字符串,以 String 形式返回错误信息。err.source()
错误来源。
如果要打印一个错误的所有可用信息,可使用下面的方法:
rust
use std::{
error::Error,
io::{stderr, Write},
};
fn print_error(mut err: &dyn Error) {
let _ = writeln!(stderr(), "error: {}", err);
while let Some(source) = err.source() {
let _ = writeln!(stderr(), "caused by: {}", source);
err = source;
}
}
writeln!
宏类似 println!
它会将数据写入所选的流。上面代码将错误消息写入了标准错误流 std::io::stderr
。 可使用 anyhow
crate 提供一个现成的错误类型。
传播错误
希望错误暂时不处理,而是沿着调用栈向上传播。使用 ?
运算符可执行此操作。
rust
let weather = get_weather(hometown)?;
- 如果结果成功,会解包 Result 以获取其中的成功值。
- 如果错误,会立即从函数返回,将错误结果沿着调用链向上传播。
在 Rust 1.13 引入 ?
运算符之前,会使用 try!()
宏。
处理多种错误类型
例如下面的代码:
rust
use std::io::{self, BufRead};
fn read_nunmbers(file: &mut dyn BufRead) -> Result<Vec<i64>, io::Error> {
let mut numbers = vec![];
for line_result in file.lines() {
let line = line_result?;
numbers.push(line.parse()?); // 报错
}
Ok(numbers)
}
line_result
的类型是 Result<String, std::io::Error>
。line.parse()
的类型是 Result<i64, std::num::ParseIntError>
。而 read_numbers()
函数的返回类型只能容纳io::Error
。Rust 试图将 ParseIntError
转换为 io::Error
,但是无法进行这样的转换,所以得到一个错误。 要解决这个问题,**方式一是自定义自己的错误类型。可以使用 **thiserror**
crate,它可以使用几行代码定义良好的错误类型。 或者 使用 Rust 中的内置特性。所有标准库中的错误类型都能转换为类型 Box<dyn std::error::Error + Send + Sync + 'static>
。 dyn std::error::Error
表示任何错误。 Send+Sync+'static
表示可以安全地在线程之间传递 。 可以通过定义类型别名来简化。
rust
type GenericError = Box<dyn std::error::Error + Send + Sync + 'static>;
type GenericResult<T> = Result<T, GenericError>;
还可考虑使用 anyhow
crate,它提供了错误类型和结果类型。 将 read_numbers
函数的返回值类型改为 GenericResult<Vec<i64>>
。这样就能根据需要自动将任意类型的错误转换为 GenericError
。 ?
会使用 From
特型以及其 from()
方法来自动转化。也可自己使用方法来转换。
rust
let io_error = io::Error::new(io::ErrorKind::Other, "time out");
return Err(GenericError::from(io_error)); // 手动转换为 GenericError
使用 GenericError
的缺点是返回类型不再准确的传达调用者可预期的错误类型。 如果正在调用一个返回 GenericResult
的函数,并且想要处理一种特定类型的错误,而让所有其他错误传播出去,那么可以使用泛型方法 error.downcast_ref::<ErrorType>()
。如果这个错误恰好是你要找的那种类型的错误,该方法就会借用对它的引用。
rust
loop {
match compile_project() {
Ok(()) => return Ok(()),
Err(err) => {
if let Some(mse) => err.downcast_ref::<MissingSenicolonError>() {
insert_semicolon_in_source_code(mse.file(), mse.line())?;
continue;
}
return Err(err);
}
}
}
处理"不可能发生"的错误
如果确信错误不可能发生,可以使用 .unwrap()
或 .expect(msg)
来进行处理,简化 Result 的处理。
忽略错误
可使用 _
来消除。
rust
let _ = writeln!(stderr(), "error: {}", err);
处理 main() 中的错误
错误通过传递,如果最后到达了 main()
函数,那么此时必须在 main()
函数中进行处理。 最简单的方式是使用 .expect()
。 也可更改 main()
的类型签名以返回 Result
类型,这样就可以使用 ?
了。
rust
fn main() -> Result<(), TideCalcError> {
let tides = calculate_tides()?;
print_tides(tides);
Ok(())
}
还有一种方式是使用 if let
。
rust
fn main() {
if let Err(err) = calculate_tides() {
print_error(&err);
std::process::exit(1);
}
}
声明自定义错误类型
自定义 JsonError
错误。
rust
#[derive(Debug, Clone)]
pub struct JsonError {
pub message: String,
pub line: usize,
pub column: usize,
}
// 错误应该能打印
use std::fmt;
impl fmt::Display for JsonError {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
write!(f, "{} ({}:{})", self.message, self.line, self.column)
}
}
// 错误应该实现 std::error::Error 特型,但使用 Error 各个方法的默认定义就够了
impl std::error::Error for JsonError {}
可以使用 thiserror
crate 来简化上面的操作。
rust
use thiserror::Error;
#[derive(Error, Debug)]
#[error("{message:} ({line:}, {column})")]
pub struct JsonDrror {
message: String,
line: usize,
column: usize
}
为什么优先选择 Result?
为什么 Rust 会优先选择 Result 而非直接触发 panic。
- Rust 要求程序员在每个可能发生错误的地方做决策,并将其记录在代码中。
- 最常见的决策是让错误继续传播,使用
?
实现。 - 是否可能出错是每个函数返回类型的一部分。
- Rust 会检查 Result 值是否被用过,这样就不会意外地让错误悄悄溜过去。
- Result 是一种数据类型,将成功结果和错误结果存储在同一个集合中。
参考链接:
🌟 🙏🙏感谢您的阅读,如果对您有帮助,欢迎关注、点赞 🌟🌟