【Rust】错误处理
Rust
的错误处理方法非常不同寻常,本节介绍了 Rust 中两种不同类型的错误处理:panic
和 Result
。
Panic
当程序遇到,数组越界,除0
,这样很严重的bug
时就会panic
,在 Result
上调用 .expect()
遇到错误以及断言失败都会发生panic
。还有宏 panic!()
,用于在代码发现它出错是,想要直接退出。panic!()
接受可选的 println!()
样式参数,用于构建错误消息。
这些都是程序员的错,但我们都会犯错,当这些不该发生的错误发生时,Rust
可以终止进程。来看一个除0
的示例:
1 | fn main() { |
运行这段代码,程序会奔溃的并且打印出调用栈,还提示我们可以设置 RUST_BACKTRACE=full
获得更多的信息:
/Users/fudenglong/.cargo/bin/cargo run --color=always --package mandelbrot --bin mandelbrot
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/mandelbrot`
thread 'main' panicked at 'attempt to divide by zero', src/main.rs:7:5
stack backtrace:
0: rust_begin_unwind
at /rustc/4ca19e09d302a4cbde14f9cb1bc109179dc824cd/library/std/src/panicking.rs:584:5
1: core::panicking::panic_fmt
at /rustc/4ca19e09d302a4cbde14f9cb1bc109179dc824cd/library/core/src/panicking.rs:142:14
2: core::panicking::panic
at /rustc/4ca19e09d302a4cbde14f9cb1bc109179dc824cd/library/core/src/panicking.rs:48:5
3: mandelbrot::pirate_share
at ./src/main.rs:7:5
4: mandelbrot::main
at ./src/main.rs:2:5
5: core::ops::function::FnOnce::call_once
at /rustc/4ca19e09d302a4cbde14f9cb1bc109179dc824cd/library/core/src/ops/function.rs:248:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
Process finished with exit code 101
线程之间的 panic
是相互独立的,也可以调用 std::panic::catch_unwind()
捕获异常,并且让程序执行。默认发生 panic
时会展开调用栈,此外有两种情况 Rust
不会尝试展开调用栈:
-
如果
.drop()
方法触发了第二次恐慌,而Rust
在第一次之后仍在尝试清理,这被认为是致命的,Rust
停止展开并中止整个进程; -
Rust
的恐慌行为是可定制的。如果使用-C panic=abort
编译,程序中的第一个panic
会立即中止进程。(使用这个选项,Rust
不需要知道如何展开调用栈,因此这可以减少编译代码的大小。)
Result
Rust
中没有异常,而是在会出现错误的函数中会返回一个 Result
类型,它预示着函数会预期执行成功,也可能因异常执失败。当我们调用函数 get_weather
的时候,要么成功返回 Ok(weather)
,weather
是 WeatherReport
的一个实例。或者出现错误时返回 Err(error_value)
,其中 error_value
是 io:Error
类型。
1 | fn get_weather(location: LatLng) -> Result<WeatherReport, io::Error> |
每当我们调用这个函数时,Rust
要求我们编写错误处理程序。如果不对 Result
做一些处理,我们就无法获取 WeatherReport
,如果未使用 Result
值,编译器就会警告。
捕获错误
处理 Result
类型最直接的方式是使用 match
表达式,这类似于其他语言中 try/catch
:
1 | match get_weather(hometown) { |
match
可以处理,但看起来似乎有点冗长。因此 Result<T, E>
提供了很多方法使用,全部方法可以阅读 https://doc.rust-lang.org/std/result/enum.Result.html
,下面是一些常用的方法列表:
-
result.is_ok(), result.is_err()
:返回一个bool
表示执行成功还是遇到错误; -
result.ok()
:以Option(T)
的形式返回成功值,如果结果是成功的则返回Some(success_value)
,否则返回None
; -
result.err()
:以Option(T)
的返回错误值; -
result.unwrap_or(fallback)
:如果有的话返回成功值,否则返回备选值,丢掉错误;1
2
3
4
5
6
7
8
// A fairly safe prediction for Southern California.
const THE_USUAL: WeatherReport = WeatherReport::Sunny(72);
// Get a real weather report, if possible.
// If not, fall back on the usual.
let report = get_weather(los_angeles).unwrap_or(THE_USUAL);
display_weather(los_angeles, &report);相比
.ok()
来说它是比较好的,因为返回的是T
而不是Option<T>
,但是只有在存在备选值得时候才有效。 -
result.unwrap_or_else(fallback_fn)
:和前面的方法是一样的,不同的是,它需要传递一个函数或闭包。这适用于在错误时有自定义逻辑处理的情况:1
2
3let report =
get_weather(hometown)
.unwrap_or_else(|_err| vague_prediction(hometown)); -
result.unwrap()
:如果result
是成功的,则返回成功值,否则将会panic
; -
result.expect(message)
:类似于unwrap()
,但是允许提供一个信息在panic
时打印; -
result.as_ref()
:将Result<T, E>
转换为Result<&T, &E>
; -
result.as_mut()
:将Result<T, E>
转换为Result<&mut T, &mut E>
;
最后这两个方法和除 .is_ok()
和 .is_err()
之外的方法不同,其他的都会消耗 result
的值,也就是它们会获取 result
的所有权,它们都是接受 self
作为参数。但是有时候我们想在不破坏数据的情况下访问数据,例如,我们想调用 result.ok()
,又想保持 result
在我们调用之后任然可用,所以我们可以编写 result.as_ref().ok()
,他只是借用 result
而不获取它的所有权,当然返回的也就是 Option<&T>
不再是 Option<T>
。
Result
别名
我们可以给 Result<T, E>
起个别名,让写起来更加简单,就像 std::fs::remove_file
函数:
1 | pub fn remove_file<P: AsRef<Path>>(path: P) -> Result<()> |
模块通常定义一个 Result
类型别名,以避免必须重复模块中几乎每个函数都一致使用的错误类型。例如,标准库的 std::io
模块包括这行代码:
1 | pub type Result<T> = result::Result<T, Error>; |
这定义了一个公共类型 std::io::Result<T>
,它是 Result<T, E>
的别名,但将 std::io::Error
硬编码为错误类型。实际上,这意味着Rust
会将 std::io::Result<String>
理解为 std::io::Result<String, std::io::Error>
的简写。
错误打印
我们经常处理错误的方式就是将错误信息打印出来,然后程序继续执行。我们之前都是用 println!()
这个宏来完成的,例如:
1 | println!("error querying the weather: {}", err); |
标注库里面提供了很多错误类型,例如 std::io::Error
,std::fmt::Error
和 std::str::Utf8Error
等等,但是它们都实现了 std::error::Error
,这意味着所有的错误都有下面的接口:
-
println!()
:所有错误类型都可以使用它打印。 使用{}
格式说明符打印错误通常只显示简短的错误消息。 或者可以使用{:?}
,以获取错误的调试视图, 这对用户不太友好,但包含额外的技术信息;1
2
3
4
5
6
7
8// result of `println!("error: {}", err);`
error: failed to lookup address information: No address associated with
hostname
// result of `println!("error: {:?}", err);`
error: Error { repr: Custom(Custom { kind: Other, error: StringError(
"failed to lookup address information: No address associated with
hostname") }) } -
err.to_string()
:返回一个错误信息作为String
; -
err.source()
:返回底层的err
。例如,网络原因导致银行交易失败,然后又导致你的转账被取消,那么err.souce()
可以返回下层的错误。
打印错误值不会同时打印出其来源。如果想确保打印所有可用信息,使用下面的代码示例:
1 | use std::error::Error; |
writeln!
宏的工作方式与 println!
类似,不同之处在于它将数据写入你选择的流。在这里,我们将错误消息写入标准错误流 std::io::stderr
。我们可以使用 eprintln!
宏做同样的事情,但 eprintln!
如果发生错误会 panic
。
错误传播
Rust
中有个 ?
操作符,用于向上传播错误。主要的应用场景是,当我们调用函数遇到错误,但又不想立即处理,只是想把这个错误继续往外传播,让最外层的调用者处理,我们就可以使用它:
1 | let weather = get_weather(hometown)?; |
?
这个操作符的行为取决于 get_weather
函数返回成功结果还是错误结果:
-
成功时,它会获取里面成功的值,也就是获取
WeatherReport
,而不是Result<WeatherReport, io::Error>
; -
出错时,它会立即返回,为了确保有效,
?
只能用于具有Result
返回类型的函数;
?
可以看做是 match
的一种简化方式:
1 | let weather = match get_weather(hometown) { |
在 Rust
较老的代码中,这个干工作是用 try!
宏处理的,直到 1.13
引入 ?
。
1 | let weather = try!(get_weather(hometown)) |
错误在程序中是非常普遍,尤其是在与操作系统接口的代码中, 因此 ?
运算符可能会出现在函数的每一行:
1 | use std::fs; |
?
也可以用于 Option
类型,道理和 Result
是相同的。
处理不同类型错误
有时候,在一个函数中会遇到多种类型的错误,而函数的返回类型是固定的,如果我们使用 ?
向上传播错误就会遇到问题,错误类型不匹配,例如:
1 | use std::io::{self, BufRead}; |
编译这段代码,会看到如下的错误,总结来说就是 line.parse()
返回的错误没法转换成 io::Error
,因为 line.parse()
返回的是 Result<i64 std::num::ParseIntError>
,ParseIntError
没法转换成 io::Error
:
error[E0277]: `?` couldn't convert the error to `std::io::Error`
--> src/main.rs:9:34
|
5 | fn read_numbers(file: &mut dyn BufRead) -> Result<Vec<i64>, io::Error> {
| --------------------------- expected `std::io::Error` because of this
...
9 | numbers.push(line.parse()?); // parsing integers can fail
| ^ the trait `From<ParseIntError>` is not implemented for `std::io::Error`
|
= note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
= help: the following other types implement trait `From<T>`:
<std::io::Error as From<ErrorKind>>
<std::io::Error as From<IntoInnerError<W>>>
<std::io::Error as From<alloc::ffi::c_str::NulError>>
= note: required because of the requirements on the impl of `FromResidual<Result<Infallible, ParseIntError>>` for `Result<Vec<i64>, std::io::Error>`
这里介绍一种错误的处理方法,Rust
标准库中的所有错误都可以转换为 Box<dyn std::error::Error + Send + Sync + 'static>
类型,其中:
-
dyn std::error::Error
:表示任意错误; -
Send + Sync + 'static
:能够在多线程之间安全传递;
出于方便,我们可以下面的类型,并且对 read_numbers()
函数进行整改,
1 | use std::io::{self, BufRead}; |
如果想从一个返回 GenericResult
的函数,找到一种特定类型的错误处理,但让其他错误传播出去,可以使用泛型方法 error.downcast_ref::<ErrorType>()
。 如果它恰好是要查找的特定类型的错误,它会借用对错误的引用:
1 | loop { |
还有一种处理方式是使用 thiserror
帮我自动实现 std::error::Error
。
忽略错误
有时候,我们可能就是想忽略一个错误,因为某个函数执行成功与否关系不大,例如写日志到 stderr
,但是我们如果不处理,编译器会报告警:
1 | writeln!(stderr(), "error: {}", err); // warning: unused result |
不过可以使用 let _ = ...
消除这个告警:
1 | let _ = writeln!(stderr(), "error: {}", err); // ok, ignore result |
处理 main
函数中的错误
使用 ?
向上传递错误大多时候是比较正确的行为,可是当错误传播到 main
函数的时候我们就需要处理。大多时候,我们看到的 main
函数签名都是下面这个样子,它的返回值类型是 ()
:
1 | fn main() { |
所以我们不能使用 ?
传播错误:
1 | fn main() { |
最简单的方式是使用 .expect()
,检查是否调用成功,如果失败就打印错误信息:
1 | fn main() { |
不过,我们也可以更改这个 main
函数的签名,让它返回 Result
类型,使用 ?
传递错误:
1 | fn main() -> Result<(), TideCalcError> { |
但是这种方式打印的错误信息不是很详细,如果想自定义错误输出,还需要自己处理错误:
1 | fn main() { |
错误定义
标注库里面的错误不可能满足所有情况,大多时候我们需要自定义错误:
1 |
|
但是如果我们希望和标准的错误类型表现一样,我们还需要做点适配:
1 | use std::fmt; |
不过,每次都实现这些 trait
是很头疼的,所以我们可使用 thiserror
,帮我自动实现:
1 | use thiserror::Error; |