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

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

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可以用于异常处理