
Rust的错误处理可能已经领先别的语言两层了,但我们甚至还没有看到终点在哪
RustConf 2020其中一场关于错误处理的报告实在是太精彩了,解决了我遇到过的很多问题,或者至少展示了前进的方向。比如,我以前用Python时写过一个subprocess的wrapper,和报告中的output2功能几乎一样,而现在我们有了Error Reporter的概念。顺便一提,应该使用ui test来测试错误输出。
但不得不说,关于错误处理,还有很多难点。这里简单地谈一下我的经验和思考。
Recoverable and unrecoverable Errors
这是一个已经被聊烂了的话题:程序bug触发panic,例如数组越界;正常行为返回Result,例如文件不存在。但实践中这个界限并不是那么清晰,例如从HashMap取值,常常就写为 &map[key],当key不存在于map中时就会panic。这个和文件不存在其实是非常相似的,那么是不是进行类似的处理就解决了呢?最大的问题在于&map[key]这种写法实在是太方便了,写成map.get(key).expect(&format!("{} not exists {}", key))就已经非常讨厌了(这么写无论是否出错都会构造错误消息,但我不知道更好的写法了,并没有一个expect_with方法);而要写成“正确”的形式map.get(key).ok_or(...)?,你需要构造一个Error,修改当前的函数返回Result,再处理一圈调用了此函数的代码,这实在是太考验人性了,而人性从来是经不起考验的。目前看来,我觉得最好的办法是标一个todo,然后视需求来处理这些todo:如果是小玩具,那就永远todo吧;如果是本地小工具,那就panic了若干次后进行修改,具体次数取决于你的耐性;如果是对正确性和响应要求高的正式项目,那就抽时间集中处理。
再明确一下,我认为&map[key]这种就类似于unwrap,除非逻辑上不可能出错出错了就意味着程序bug,在正式代码里是不应该出现的;尽量使用Error而不是panic。
Fehler
当然我们还是有一些办法来缓解这个问题的。其中我觉得正确得很明显的一个是fehler,Ok-Wrapping实在是太香了,把一个函数从返回T改成返回Result<T, E>只需要加一行attribute。
Anonymous sum types
定义error type是另一个痛点。很多时候一个crate只有一个error type,我理解这是人在懒惰和正确之间做的一个妥协。看到相当一部分人觉得snafu是优于thiserror的库,比如最近有这篇文章;我还没有仔细地对比过两者,但至少有一点我觉得snafu很正确:每个moduel都有单独的Error,而不是整个crate只有一个。更极端地说,我认为(几乎)每个函数都应该有一个Error。这样当然非常繁琐,对于想偷懒的人,Anonymous sum types会非常有用 。而anyhow!这种一次性的error type,本质上就是一种只能用于报告的error type,可能大部分用处都会被取代。
Errors intended for an application's user and errors intended to be handled by code
两者的界限同样并不是特别清晰,原来写在main.rs里的函数可能会被放到单独的文件里,然后再被提取为单独的library。极端一点,所有binary crate的main.rs都应该只是对同级的lib.rs的简单调用,所以理论上几乎不存在仅仅展示用的Error。当然实际上的情况往往恰好相反,能够用anyhow!返回字符串而不是直接unwrap就已经很好了。不过我还是同意应该区分纯报告给用户(于是可以偷懒!)的error和需要代码处理的error的,但区分的标准不是这个error产生于library还是application,而是语义上这个error应该如何处理:library也可以产生前一种error,application也可以产生后一种error。
Warning
Warning是一个Rust社区很少讨论的和error很接近的概念。它们可能会互相转化,之前觉得是硬错误的问题,后来觉得可以继续执行只要log输出一下就行了,反之亦然。核心的一点是,warning同样会需要context、source、backtrace这些以帮助人理解发生了什么。我用过w_result这个库,还是挺好用的。
Vec<Error>
Vec<Warning>的概念比较明显,同样的也有Vec<Error>的问题,调用者有可能需要一次性收集所有错误(尤其是要报告给人的时候)。某种意义上,error可以分为中断性的error和可以继续于是可收集的error,而这两者有时也是根据语义的。而Result<T, Vec<Error>>是和Result<T, Error>不兼容的;或许每个Error Enum都应该有一个List的variant?我知道一个相关的库beau_collector,但它的适用范围并不广。
Async Error
Elm之前版本的教程里提到了用Task优雅地处理异步错误,但具体内容一直是comming soon,然后最新版本整个Error Handling一章都消失了。顺便一提,Rust现在如此优秀的编译错误提示也是来源于elm的。回到Rust,以我极为有限的了解,基本最终还是panic了事。这事可能还没人想明白。
Conclusion
<del>Errors are annoying</del>
整理了一下思路,感觉中断性/可以继续的error/warning可能全都是同一个东西。一个返回T的函数,其实应该返回WResult<T, E, E>,其中E大概是这样的定义:
enum Error {
E1,
E2,
List(Vec<Box<Error>>),
}
函数自身决定返回WOk还是WErr,调用者在收到WErr时只能继续传播或处理,而收到带Warning的WOk时,根据每个具体的Error决定传播(加入自己的Warning或Error列表)还是处理(打log、哪怕是直接丢弃)。近期争取抽空来测试一下这个想法。
这篇博客深入探讨了Rust语言的错误处理机制,包括Result与panic的区别、如何处理可恢复与不可恢复的错误、错误类型的定义以及在不同场景下的最佳实践。作者提出将错误分为中断性与可继续的类型,并建议使用WResult来统一处理。此外,还讨论了异步错误处理的复杂性以及与警告的关系。文章提供了fehler库和anonymoussumtypes等工具作为解决方案,并强调了错误和警告在语义上的重要性。

2万+

被折叠的 条评论
为什么被折叠?



