错误处理是程序设计语言中的重要组成部分,是程序开发工作中最重要,也最容易出问题的地方之一。语言的错误处理机制体现了该语言的特点。
错误处理主要分为以下几种
1. 使用全局错误来作为错误处理
2. 使用返回值做为错误处理
3. 使用异常来做错误处理
4. 使用范畴论中的Mond
下面将大概介绍这4种方式,再介绍Rust的错误处理的特殊性
1. 使用全局错误来作为错误处理
c语言采用了这种方式,此种方式当错误发生时,函数调用会返回NULL, 错误原因会记录到 全局变量errno中。
errno ; { FILE * pf; errnum; pf = fopen (, ); (pf == ) { errnum = errno; (, , errno); (, , strerror( errnum )); } { fclose (pf); } ; }
此种方式的缺点非常明显,使用全局变量errno记录错误原因,很容易引起问题。
2. 使用返回值做为错误处理
Go 语言采用了此种方式
{ src, err := os.Open(srcName) err != { } dst, err := os.Create(dstName) err != { } written, err = io.Copy(dst, src) dst.Close() src.Close() }
Go需要在每个函数调用的地方判断是否有 err = nil, 这样的好处是每个函数的错误都可以被处理。但缺点也是非常明显,代码中大量
充斥着判断err的代码,导致代码非常丑陋,而且可能很多地方基本上不需要关注错误,只需要在最顶层处理错误,而不是在调用链上每个地方判断
3.使用异常
java/c#等采用了这种机制
{ (b == ) { IllegalArgumentException(); } a/b; }
这种方式的优点是,只要有异常,上层调用如果不关心异常,则可不需要处理,如果关心异常则可以捕获异常做相应的业务处理,非常的灵活。
缺点是,调用者如果不看方法签名注释或者源码,则不会知道该方法是否会抛出异常,一旦忘了处理该异常,则有可能会产生bug.
4. 使用范畴论中的Mond
Haskell使用这种方式,该模式使用ADT来封装错误,将错误包裹到容器中
divBy :: Integral a => a -> [a] -> Maybe [a]
divBy _ [] = Just []
divBy _ (0:_) = Nothing
divBy numerator (denom:xs) =
case divBy numerator xs of
Nothing -> Nothing
Just results -> Just ((numerator `div` denom) : results)
调用方在调用divBy函数时,需要对结果进行模式匹配。这种方式是显式的错误处理,调用者必选处理错误,错误被包裹到Mond中,
使用Mond的一序列操作符来处理错误。缺点是丢失了原始的错误位置信息,错误需要在Mond中处理,需要代码架构上对Mond友好。
Rust中的错误处理方式
Rust是一门多范式的语言,错误处理吸收了Haskell, Scala的特点。Rust中的错误也是包裹到Mond中,不同的是错误不需要做模式匹配就可以从Mond中取出,
另外Rust的错误处理设计还兼具异常设计的特点,调用方如果不关注错误,则可以像java中向上将异常冒泡。
Rust错误处理的核心是std::result::Result
<, > { (), (), }
Rust中,可以使用模式匹配来处理错误,也可以从Result中取出数据或错误
(){ cost = get_cost(, ); cost { (price) => (, price), (err) => (, err) }
cost.is_ok() { (, cost.unwrap()); } { (, cost.err().unwrap()); }
} (num_err:, price_err:) -> <, > { number = get_number(num_err); number { (n) => { price = get_price(price_err); price { (p) => (n * p), (err) => (err) } }, (err) => (err) } } (err:)-> <, > { !err { () } { (.to_string()) } } (err:) -> <, > { err { () } { (.to_string()) } }
有没有发现,上面的代码比较繁琐,模式匹配导致容易产生大量的嵌套代码,unwrap是一种不够优雅的方式,如果code review不够仔细,可能一些unwrap会导致致命异常。
幸运的是Rust提供了try!宏,使用该宏可以让异常提前返回,效果上类似java异常。rust也支持了try!宏的语法糖?, 大大方便了程序的编写,以下使用?来重写上面的例子。
必须注意的是使用try!(?)的关键是函数返回值类型必须是std::result::Result<T,E>
() -> <(), > { cost = get_cost(, ); (, cost); (()) } (num_err:, price_err:) -> <, > { number = get_number(num_err); price = get_price(price_err); (number * price) } (err:)-> <, > { err { () } { (.to_string()) } } (err:) -> <, > { err { () } { (.to_string()) } }
Rust的异常体系中 std::error::Error,是一个核心Trait, 任何实现了该Trait的struct, 标准库都将自动为其实现From, 这样该struct就可以转换为Box<Error + 'a>,如下
impl<'a, E: Error + 'a> From<E> for Box<Error + 'a>
也就是说,如果某个struct, MyError实现了std::error::Error,并且某个方法返回类型为 std::result::Result<T1, MyError>, 且调用者返回的类型为
std::result::Result<T1, Box<std::error::Error>>,则调用方也可以使用try!宏,还是之前的例子,稍微改动下
{ : } Display MyError{ (&, f: &Formatter<>) -> std::fmt:: { (f, , .) } } Error MyError{} () -> <(), <Error>> { cost = get_cost(, ); (, cost); (()) } (num_err:, price_err:) -> <, <Error>> { number = get_number(num_err); price = get_price(price_err); (number * price) } (err:)-> <, MyError> { err { () } { (MyError{:.to_string()}) } } (err:) -> <, MyError> { !err { () } { (MyError{:.to_string()}) } }
可以看到 std::error:::Error充当了 java/c#中异常基类的作用,任何实现了Error的struct/enum, 方法的返回值只要声明为Result<T, MyError>,且调用者的方法返回值也是Result<T, Box<dyn Error>>,则调用者可以使用try!或?语法糖使异常提前返回。
在实战中,一搬会定义一个业务错误类 BussinessError,实现Error trait, 然后所有的方法返回 Result<T, BussinessError>,这样所有的方法都可以使用?语法糖了。当然当调用第三方类库或者调用标准库时,需要将对应的异常转为BussinessError。
如果觉得自己定义异常基类比较繁琐,可以使用第三方类库,比如anyhow, 该类库定义了很多异常模式,可以直接拿来使用,以下是使用anyhow改写后的例子
() -> anyhow::<()> { cost = get_cost(, ); (, cost); (()) } (num_err:, price_err:) -> anyhow::<> { number = get_number(num_err); price = get_price(price_err); (number * price) } (err:)-> anyhow::<>{ err { () } { anyhow::() } } (err:) -> anyhow::<> { !err { () } { anyhow::() } }
除了anyhow,还有很多其他类库 如 thiserror, derive-error可以用于异常处理