电子说
编写程序,错误处理是不可避免的。
但程序员总是偏向正常的情况,而容易忽略有错误的情况。
返回值错误处理
最开始,错误是通过返回值来表示,比如非零表示错误,0表示成功。而处理错误的代码类似
这样的代码,错误处理代码和业务逻辑交织在一起,也容易忽略处理错误。以及把返回值只用于错误返回,有点浪费的感觉。因为很多时候把计算结果作为返回值,更符合思考的逻辑。
异常错误处理
后面出现了异常的方式,在出错的时候,抛出异常。异常一层一层往上抛,如果没有处理异常,那么程序就会被terminate. 比如C++和Java采用这种方式。
使用异常的代码类似
看起来错误处理代码与业务逻辑分开,比较清晰。但有如下的不足,
错误处理也容易被忽略,不写相应的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
一个是没有错误时的计算结果
一个是出错时,要返回的错误
第一点,我们可以看到,现在返回值可以用于返回函数计算的结果了,没有被错误占领。
第二点,因为返回的值又不是计算结果,所以程序员不能直接使用返回值,需要先检查具体的类型,没有出错时,才能使用计算结果。这样又避免了无意识的忽略错误。
我们可以简陋地认为Result类型,是C++里面的tag union,即包含一个tag的union。其中tag是错误标记,如果是0表示成功,非零表示错误,而union则存放着具体的错误或者具体的计算结果。(很多时候Result,称作是和类型 sum type)
可以避免无意识地忽略错误,那么可读性呢?
因为返回值不是计算结果,需要检查一下才能继续下一步,这不就跟错误返回值一样了吗?
注:先把话说明,没有错误处理的代码是可读性最好的。因为只有happy path,第一步,第二步等等。但我们讨论在可能出错的时候的可读性。
Result和类型的代码可以是
哇咔咔,这看上去可读性很差那。实话说,这么写的代码的确没有什么可读性。
但Rust提供了另外一个写法,如下
let res = step1()?;let res= step2()?;let res = step3()?;
这个写法看起来很像异常的情况。业务逻辑和错误处理没有交织在一起。
眼尖的读者会发现每个函数都有个问号?。而错误处理就藏在?后面。
问号的存在,让Rust自动帮你检查返回值,在出错的时候直接返回错误,不再继续往下走了。问号可以展开为如下的形式(简化版本,方便理解,实际版本请看官方文档),
到这里,我们可以看到Rust的创新点在于将错误与计算结果放在了返回值,而不是单纯地返回错误,或者返回计算结果和从第三个路径返回异常。并且提供了问号和组合子来简写错误处理。所以同时提供了避免无意识忽略错误和提供可读性。
但错误处理远远不止这点内容。在我写了GitHub的webhook微服务 https://github.com/Celthi/github-webhook-gateway 以后,我发现写了一大坨下面的代码
写成这样,说明我对Rust的错误处理仍然没有理解到位,于是我试着重构这段代码,并提了个问题How reduce the nested if and indents?
经过重构以后,我发现了如下的一些情况
有时候只想处理成功的情况,我称之为“最大努力做事”。所以代码逻辑是这样
这也是我自己代码那么多缩进的原因。它可以通过如下方式来改善,
方式一、首先先把代码段提到一个单独的函数post_sending_task(),然后将返回值改成Result,所以调用的地方代码是
let _ = best_delivery(); //这里使用使用_,说明我们不关心失败的情况
在这个best_delivery()里面,我们就可以使用问号表达式了。
方式二、使用组合子,如将Option转换成Result,从而可以使用问号,如
let res = get_something().ok_or_else(|| err)?;
这里ok_or_else是option上的组合子。什么是组合子,简单理解是将东西组合在一起的函数。至于”子“,一种称谓罢了,要说相似的话,第一反应类似套接字里面的”字“的功能。
方式三、提前返回。通过反转if的条件,提前返回,比如,
提前返回没有问号那么可读性强,但是减少了缩进的层数。
方式四、如果获取结果的同时必须处理错误的情况,那么使用下面的形式,
注意,问号表达式是适合于获取结果且不处理错误,直接往上抛。
经过这四个个方式的改善,我的代码可读性提高了,变成了
错误处理与日志、错误报告
错误处理的时候,通常要写日志。但是错误处理和日志是两码事。不是所有的错误处理都要写日志,而且不同的错误,写到的日志级别是不一样的,如调试,信息,错误,严重等等级别。
错误处理是处理出错的情况,而日志是记录感兴趣的信息。它们有重合,但是关注点不一样。以后再写文章。
错误报告(error report)跟错误处理也是两码事,虽然经常关联在一起,也留作以后再写文章。
审核编辑:刘清
全部0条评论
快来发表一下你的评论吧 !