查看原文
其他

Rust错误处理

秋风不度镇南关 码农真经 2023-12-25

错误处理是程序设计语言中的重要组成部分,是程序开发工作中最重要,也最容易出问题的地方之一。语言的错误处理机制体现了该语言的特点。

错误处理主要分为以下几种

1. 使用全局错误来作为错误处理

2. 使用返回值做为错误处理

3. 使用异常来做错误处理

4. 使用范畴论中的Mond

下面将大概介绍这4种方式,再介绍Rust的错误处理的特殊性

1. 使用全局错误来作为错误处理

c语言采用了这种方式,此种方式当错误发生时,函数调用会返回NULL, 错误原因会记录到 全局变量errno中。

#include <stdio.h>
#include <errno.h>
#include <string.h>

extern int errno ;
int main ()
{
FILE * pf;
int errnum;
pf = fopen ("unexist.txt", "rb");
if (pf == NULL)
{
errnum = errno;
fprintf(stderr, "errno: %d\n", errno);
fprintf(stderr, "open file error: %s\n", strerror( errnum ));
}
else
{
fclose (pf);
}
return 0;
}

此种方式的缺点非常明显,使用全局变量errno记录错误原因,很容易引起问题。

2. 使用返回值做为错误处理

Go 语言采用了此种方式

func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}

dst, err := os.Create(dstName)
if err != nil {
return
}

written, err = io.Copy(dst, src)
dst.Close()
src.Close()
return
}

Go需要在每个函数调用的地方判断是否有 err = nil, 这样的好处是每个函数的错误都可以被处理。但缺点也是非常明显,代码中大量

充斥着判断err的代码,导致代码非常丑陋,而且可能很多地方基本上不需要关注错误,只需要在最顶层处理错误,而不是在调用链上每个地方判断

3.使用异常

java/c#等采用了这种机制

public float divide(int a, int b) {
if(b == 0) {
throw new IllegalArgumentException("b can't be 0");
}
return 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

pub enum Result<T, E> {
/// Contains the success value
#[lang = "Ok"]
#[stable(feature = "rust1", since = "1.0.0")]
Ok(#[stable(feature = "rust1", since = "1.0.0")] T),

/// Contains the error value
#[lang = "Err"]
#[stable(feature = "rust1", since = "1.0.0")]
Err(#[stable(feature = "rust1", since = "1.0.0")] E),
}

Rust中,可以使用模式匹配来处理错误,也可以从Result中取出数据或错误

fn main(){
let cost = get_cost(true, false);
match cost {
Ok(price) => println!("price is {}", price),
Err(err) => println!("{}", err)
}

//或者取出错误
if
cost.is_ok() {
println!("price is {}", cost.unwrap());
}
else {
println!("{}", cost.err().unwrap());
}}
fn get_cost(num_err:bool, price_err:bool) -> Result<u32, String> {
let number = get_number(num_err);
match number {
Ok(n) => {
let price = get_price(price_err);
match price {
Ok(p) => Ok(n * p),
Err(err) => Err(err)
}
},
Err(err) => Err(err)
}

}
fn get_number(err:bool)-> Result<u32, String> {
if !err {
Ok(100)
} else {
Err("Failed to get num".to_string())
}
}
fn get_price(err:bool) -> Result<u32, String> {
if !err {
Ok(100)
} else {
Err("Failed to get price".to_string())
}
}

有没有发现,上面的代码比较繁琐,模式匹配导致容易产生大量的嵌套代码,unwrap是一种不够优雅的方式,如果code review不够仔细,可能一些unwrap会导致致命异常。

幸运的是Rust提供了try!宏,使用该宏可以让异常提前返回,效果上类似java异常。rust也支持了try!宏的语法糖?, 大大方便了程序的编写,以下使用?来重写上面的例子。

必须注意的是使用try!(?)的关键是函数返回值类型必须是std::result::Result<T,E>

fn main() -> Result<(), String> {
let cost = get_cost(true, false)?;//这里如果返回Err,则函数会提前返回
println!("cost is {}", cost);
Ok(())
}
fn get_cost(num_err:bool, price_err:bool) -> Result<u32, String> {
let number = get_number(num_err)?;//这里如果返回Err,则函数会提前返回
let price = get_price(price_err)?;//这里如果返回Err,则函数会提前返回
Ok(number * price)
}
fn get_number(err:bool)-> Result<u32, String> {
if err {
Ok(100)
} else {
Err("Failed to get price".to_string())
}
}
fn get_price(err:bool) -> Result<u32, String> {
if err {
Ok(100)
} else {
Err("Failed to get price".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!宏,还是之前的例子,稍微改动下

#[derive(Debug)]
struct MyError{
msg:String
}
impl Display for MyError{ //实现 Error 必须先实现 Display
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.msg)
}
}
impl Error for MyError{}

fn main() -> Result<(), Box<dyn Error>> {
let cost = get_cost(true, false)?;
println!("cost is {}", cost);
Ok(())
}
fn get_cost(num_err:bool, price_err:bool) -> Result<u32, Box<dyn Error>> {
let number = get_number(num_err)?;// MyError实现了 Error trait, 标准库又为实现了Error trait的struct,实现了impl From for Box<dyn Error>,
let price = get_price(price_err)?;// 这样就可使用try!了
Ok(number * price)
}
fn get_number(err:bool)-> Result<u32, MyError> {
if err {
Ok(100)
} else {
Err(MyError{msg:"Failed to get price".to_string()})
}
}
fn get_price(err:bool) -> Result<u32, MyError> {
if !err {
Ok(100)
} else {
Err(MyError{msg:"Failed to get price".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改写后的例子

fn main() -> anyhow::Result<()> {
let cost = get_cost(true, false)?;
println!("cost is {}", cost);
Ok(())
}
fn get_cost(num_err:bool, price_err:bool) -> anyhow::Result<u32> {
let number = get_number(num_err)?;
let price = get_price(price_err)?;
Ok(number * price)
}
fn get_number(err:bool)-> anyhow::Result<u32>{
if err {
Ok(100)
} else {
anyhow::bail!("Failed to get price")
}
}
fn get_price(err:bool) -> anyhow::Result<u32> {
if !err {
Ok(100)
} else {
anyhow::bail!("Failed to get price")
}
}

除了anyhow,还有很多其他类库 如 thiserror, derive-error可以用于异常处理


继续滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存