文章目录
错误处理机制的设计是程序架构设计中非常重要的一部分。任何可能的错误,只要没有得到妥善的处理,日后都可能成为系统的隐患。
程序中,错误处理的基本流程为:
- 当错误发生时,用适当的方式捕获这个错误;
- 对捕获的错误可以立即进行处理,也可以对错误进行传播(propagate),直到不得不处理的地方再处理。
- 最终,对内做好错误信息的日志记录,对外给用户展示友好的、易于理解问题所在的错误信息。
几种不同的错误处理方式
使用返回值(错误码)
以C语言为代表,使用函数返回值作为错误记录和传播的手段。
缺点是,开发者需要根据准确的文档,以显示编程的方式处理(或进一步传递)各种返回情况。
然而,不得忽视的一个事实是,代码更新后往往很难保持文档的实时准确更新。
使用异常(Exception)
以Java语言为代表。
程序中任何可能出错的地方,都可以抛出异常;而异常可以通过栈回溯(stack unwind
)被一层层自动传递,直到遇到捕获异常的地方,如果回溯到main
函数还无人捕获,程序就会异常退出。
引入异常来处理错误,其优点是:错误的产生和错误的处理完全被分隔开(关注点分离),调用者不必关心错误,而被调者也不强求调用者关心错误。
不过,使用异常也有缺点:
- 在需要打开/关闭资源的情况下,容易出现因为抛出异常而资源没有得到正确关闭和释放的问题。这一点需要编程时特别小心,而且处理起来比较繁琐。
- 异常容易被滥用。毕竟,使用异常的代价要远比处理返回值的代价高。
使用类型系统
Rust语言中,就主要使用类型系统来传播和处理错误。主要使用到内置的Option<T>
和Result<T,E>
类型。
同时,当出现比较严重、不可恢复的错误时,Rust支持用panic!
宏抛出异常,用来快速退出程序,或者进入外围的catch_unwind
代码中。
Option< T >
标准库中的定义:
pub enum Option<T> {
None,
Some(T),
}
Option<T>
是一个枚举类型,要么是Some<T>
,要么是None
。这能很好地表达有值和无值两种情况,避免出现Java中的NullPointerException
。
Result< T, E >
#[must_use = "this `Result` may be an `Err` variant, which should be handled"]
pub enum Result<T, E> {
/// Contains the success value
Ok(T),
/// Contains the error value
Err(E),
}
Result<T,E>
是也一个枚举类型,要么是正常情况下的结果类型T,要么是错误情况下的结果类型E。
注意一下 #must_use
标注,编译器会对有带有此标注的所有类型做特殊检查:如果该类型对应的值没有在代码中显式地使用,编译器会给出有警告。这能提醒开发者需要将所有错误显示处理。
?操作符
为了让Result<T,E>类型的处理更加方便简洁,Rust中引入了?
操作符。
?操作符本质是一个类似如下代码的match匹配。
match result {
Ok(v) => v,
Err(e) => return Err(e.into())
}
也就是说:
- 如果
Result
是一个E类型
的错误值,则提前返回错误,结束当前函数。 - 如果
Result
是一个T类型
的正确值,则提取出值,方便后续进行链式调用。
代码测试
先来定义一个自己的错误类型:
struct MyError {
code: i32,
msg: String
}
use std::fmt::{Display,Debug,Formatter};
impl std::error::Error for MyError { }
impl Debug for MyError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}]{}", self.code, self.msg)
}
}
impl Display for MyError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}]{}", self.code, self.msg)
}
}
在main()
函数中测试:
fn main() -> Result<(), MyError> {
let oneError: Result<(), MyError>= Err(MyError{
code: -1,
msg: "error".to_owned(),
});
// 问号操作符是针对Result<T,E>类型的一个语法糖
// 本质是一个match匹配
// 如果Result是一个E类型的错误值,则提前返回错误,结束当前函数
// 如果Result是一个T类型的正确值,则提取出值,方便后续进行链式调用
oneError?;
Ok(())
}
?操作符的使用限制
- ?操作符只能使用在以Option或者Result作为返回值的函数体中。
- 如果要在main()中使用?操作符。那么首先是要求
main()
返回值是Option
或者Result
类型(满足第1条);其次,还要求返回值是要实现std::process::Termination trait
的类型。
rust文档:Termination
pub trait Termination {
fn report(self) -> i32;
}
其中report()
方法返回一个i32
值,这与C语言中main()
函数要求返回一个整数值一致。
测试代码中的Result<(), MyError>
可以作为main函数的返回值类型,是因为标准库中有如下实现:
impl<E: fmt::Debug> Termination for Result<(), E> {
fn report(self) -> i32 {
match self {
Ok(()) => ().report(),
Err(err) => Err::<!, _>(err).report(),
}
}
}
panic!和catch_unwind
当出现比较严重、不可恢复的错误时,Rust支持用panic!
宏抛出异常,用来快速退出程序。
如果某些情况下,不希望程序内部的panic!导致程序退出,可以将程序代码包裹在一个闭包中,然后传入std::panic::catch_unwind
中执行。
use std::panic;
let result = panic::catch_unwind(||{
println!("Hello");
});
println!("{}", result.is_ok());//true
let result = panic::catch_unwind(||{
// 打印异常堆栈信息
// 被catch_unwind包裹,该panic不会导致进程退出
panic!("Error in catch_unwind");
});
if(result.is_err()) {
// 发现异常
println!("maybe do something");
}
println!("before panic");//before panic
//让程序异常退出
panic!("error exit");
//上一句代码已导致程序异常退出,这一句执行不到
println!("normal exit");