Rust代码启发之返回值异常错误处理

电子说

1.3w人已加入

描述

编写程序,错误处理是不可避免的。

但程序员总是偏向正常的情况,而容易忽略有错误的情况。

返回值错误处理

最开始,错误是通过返回值来表示,比如非零表示错误,0表示成功。而处理错误的代码类似

Rust

这样的代码,错误处理代码和业务逻辑交织在一起,也容易忽略处理错误。以及把返回值只用于错误返回,有点浪费的感觉。因为很多时候把计算结果作为返回值,更符合思考的逻辑。

异常错误处理

后面出现了异常的方式,在出错的时候,抛出异常。异常一层一层往上抛,如果没有处理异常,那么程序就会被terminate. 比如C++和Java采用这种方式。

使用异常的代码类似

Rust

看起来错误处理代码与业务逻辑分开,比较清晰。但有如下的不足,

错误处理也容易被忽略,不写相应的catch,

上层调用者在嵌套很多层的时候,很难知道底层是否会抛异常。

这么写代码,在catch的地方,分不清楚是在哪里出了错误,step1?step3?

注:python把异常还用于程序控制流改变,如StopInteractionException用于跳出循环。

Java里异常还分checked exception和unchecked exception。checked exception是必须要处理的异常,从而可以避免被忽略。但checked exception有其局限性,比如添加新的checked exception,会改变接口签名,变得不能向前兼容。

综上,我们需要一种错误处理

避免无意识地忽略。

可阅读性强。

其中返回值和异常都可能会被无意识忽略。可读性,异常好于返回值,且避免占用了返回值。而不可忽略的Java checked exception有它自己的问题。

就没有其他更好的方式了吗?Rust给出了它的答案,使用Result 类型。

什么是Result和类型?

Result的完整形态是Result,其中T和E是泛型参数。不懂泛型不重要,这里跟泛型没有关系。我们要知道的是Result是两个类型的集合:

一个是没有错误时的计算结果

一个是出错时,要返回的错误

第一点,我们可以看到,现在返回值可以用于返回函数计算的结果了,没有被错误占领。

第二点,因为返回的值又不是计算结果,所以程序员不能直接使用返回值,需要先检查具体的类型,没有出错时,才能使用计算结果。这样又避免了无意识的忽略错误。

我们可以简陋地认为Result类型,是C++里面的tag union,即包含一个tag的union。其中tag是错误标记,如果是0表示成功,非零表示错误,而union则存放着具体的错误或者具体的计算结果。(很多时候Result,称作是和类型 sum type)

可以避免无意识地忽略错误,那么可读性呢?

因为返回值不是计算结果,需要检查一下才能继续下一步,这不就跟错误返回值一样了吗?

注:先把话说明,没有错误处理的代码是可读性最好的。因为只有happy path,第一步,第二步等等。但我们讨论在可能出错的时候的可读性。

Result和类型的代码可以是

Rust

哇咔咔,这看上去可读性很差那。实话说,这么写的代码的确没有什么可读性。

但Rust提供了另外一个写法,如下

let res = step1()?;let res= step2()?;let res = step3()?;

这个写法看起来很像异常的情况。业务逻辑和错误处理没有交织在一起。

眼尖的读者会发现每个函数都有个问号?。而错误处理就藏在?后面。

问号的存在,让Rust自动帮你检查返回值,在出错的时候直接返回错误,不再继续往下走了。问号可以展开为如下的形式(简化版本,方便理解,实际版本请看官方文档),

Rust

到这里,我们可以看到Rust的创新点在于将错误与计算结果放在了返回值,而不是单纯地返回错误,或者返回计算结果和从第三个路径返回异常。并且提供了问号和组合子来简写错误处理。所以同时提供了避免无意识忽略错误和提供可读性。

但错误处理远远不止这点内容。在我写了GitHub的webhook微服务 https://github.com/Celthi/github-webhook-gateway 以后,我发现写了一大坨下面的代码

Rust

写成这样,说明我对Rust的错误处理仍然没有理解到位,于是我试着重构这段代码,并提了个问题How reduce the nested if and indents?

经过重构以后,我发现了如下的一些情况

有时候只想处理成功的情况,我称之为“最大努力做事”。所以代码逻辑是这样

Rust

这也是我自己代码那么多缩进的原因。它可以通过如下方式来改善,

方式一、首先先把代码段提到一个单独的函数post_sending_task(),然后将返回值改成Result,所以调用的地方代码是

let _ = best_delivery(); //这里使用使用_,说明我们不关心失败的情况

在这个best_delivery()里面,我们就可以使用问号表达式了。

方式二、使用组合子,如将Option转换成Result,从而可以使用问号,如

let res = get_something().ok_or_else(|| err)?;

这里ok_or_else是option上的组合子。什么是组合子,简单理解是将东西组合在一起的函数。至于”子“,一种称谓罢了,要说相似的话,第一反应类似套接字里面的”字“的功能。

方式三、提前返回。通过反转if的条件,提前返回,比如,

Rust

提前返回没有问号那么可读性强,但是减少了缩进的层数。

方式四、如果获取结果的同时必须处理错误的情况,那么使用下面的形式,
Rust

注意,问号表达式是适合于获取结果且不处理错误,直接往上抛。

经过这四个个方式的改善,我的代码可读性提高了,变成了

Rust

错误处理与日志、错误报告

错误处理的时候,通常要写日志。但是错误处理和日志是两码事。不是所有的错误处理都要写日志,而且不同的错误,写到的日志级别是不一样的,如调试,信息,错误,严重等等级别。

错误处理是处理出错的情况,而日志是记录感兴趣的信息。它们有重合,但是关注点不一样。以后再写文章。

错误报告(error report)跟错误处理也是两码事,虽然经常关联在一起,也留作以后再写文章。

 



 



审核编辑:刘清

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分