语言优秀的错误处理能力,会大大减少错误对整体流程的破坏,减少我们码农的心智负担。
我们一般处理错误的流程:
主要有三种方法:
有很多例子 比如:
在 C 语言中,如果 fopen(filename) 无法打开文件,会返回 NULL,调用者通过判断返回值是否为 NULL,来进行相应的错误处理。
这样有很多局限,返回值本来有自己的语义,非要把错误和返回值混淆在一起,加重了开发者的心智负担。
Golang对返回值做了扩展,可以返回多个参数,可以返回专门的error类型。
func Fread(file *File, b []byte) (n int, err error)
这样就可以把错误和正常的返回区分开来了。 这样一来这个err就会在调用链中显式传播。 所以在Golang的代码中随处可见的
if err != nil {
// 错误处理……
}
由于返回值不利于错误的传播,Java等语言使用异常来处理错误。
异常可以看成关注点分离:错误的产生和处理是分隔开的,调用者不必关心错误。
程序中任何可能出错的地方,都可以抛出异常;而异常可以通过栈回溯(stack unwind)被一层层自动传递,直到遇到捕获异常的地方,如果回溯到 main 函数还无人捕获,程序就会崩溃。如下图所示:
这样可以简化错误处理流程,解决了返回值传播的问题。
用异常更容易写代码,但当异常安全无法保证时,程序的正确性会受到很大的挑战。 可是保证异常安全的第一个原则就是:避免抛出异常。
异常处理另外一个比较严重的问题是:开发者会滥用异常。异常处理的开销要比处理返回值大得多,滥用会有很多额外的开销。
错误信息既然可以通过已有的类型携带,或者通过多返回值的方式提供,那通过类型来表征错误,用一个内部包含正常返回类型和错误返回类型的复合类型,通过类型系统来强制错误的处理和传递效果更好。(Golang 好像就是这样)
但我们前面提到用返回值返回错误的缺点:错误需要被调用者立即处理,或显式传递。 用类型来处理错误的好处是:可以用函数式编程,简化错误的处理。 如:map、fold等函数,让代码不那么冗余。
Rust总结前辈的经验,使用类型系统来构建主要的错误处理流程。 构建了Option类型和Result类型。 代码定义如下:
pub enum Option<T> {
None,
Some(T),
}
#[must_use = "this `Result` may be an `Err` variant, which should be handled"]
pub enum Result<T, E> {
Ok(T),
Err(E),
}
可以看到Result类型有must_use, 如果没有使用就会报warning,以保证错误被处理了。
上图中的例子,如果我们不处理read_file的返回值,就开始有提示了。 (那这不是回到了 Golang的 到处都是 if err != nil的情况了吗?)
如果执行传播错误,不想当时处理,就用?操作符。这样让错误传播和异常处理不相上下,同时又避免了异常处理带来的问题。
use std::fs::File;
use std::io::Read;
fn read_file(name: &str) -> Result<String, std::io::Error> {
let mut f = File::open(name)?;
let mut contents = String::new();
f.read_to_string(&mut contents)?;
Ok(contents)
}
?操作符 展开来就类似这样:
match result {
Ok(v) => v,
Err(e) => return Err(e.into())
}
我们就能写出这样的函数式编程的代码。
fut
.await?
.process()?
.next()
.await?;
流程如图:
注意: 在不同错误类型之间是无法直接使用的,需要实现From trait在二者之间建立转换桥梁。
Result<T, E> 里 E 是一个代表错误的数据类型。 为了规范这个代表错误的数据类型的行为,Rust 定义了 Error trait:
pub trait Error: Debug + Display {
fn source(&self) -> Option<&(dyn Error + 'static)> { ... }
fn backtrace(&self) -> Option<&Backtrace> { ... }
fn description(&self) -> &str { ... }
fn cause(&self) -> Option<&dyn Error> { ... }
}
也可以自定义数据类型,然后实现 Error trait。 幸运的是,我们可以用thiserror和anyhow来简化这些步骤。
thiserror 提供了一个派生宏(derive macro)来简化错误类型的定义.
use thiserror::Error;
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum DataStoreError {
#[error("data store disconnected")]
Disconnect(#[from] io::Error),
#[error("the data for key `{0}` is not available")]
Redaction(String),
#[error("invalid header (expected {expected:?}, found {found:?})")]
InvalidHeader {
expected: String,
found: String,
},
#[error("unknown data store error")]
Unknown,
}
错误处理的三种方式:使用返回值、异常处理和类型系统。而 Rust 目前看到的方案:主要用类型系统来处理错误,辅以异常来应对不可恢复的错误。