crate failure 的五种错误模式
总结一下failure曾给出的五种错误设计模式。他们适合什么场景?现在有更好的选择吗?
五种模式解析
String
即每个函数返回的类型为Result<T, String>
示意图:
优点:不费脑子,基础设施代码少,方便快捷一把梭,?可以将各种错误转换为字符串
缺点:
- scattering:错误信息字符串散乱在代码各处,维护起来比较困难
- brittle:如果调用者要针对不同的错误做特殊处理,只能靠字符串匹配,要是你改变了错误信息,下游的错误处理全部崩盘
适用场景: 原型期,非lib工程,或者适用于那些十分罕见的错误,如果遇到要么写入日志,要么跳过,不会单独识别出来做特殊处理
Using the Error type
即函数的返回类型为Result<T, Error>
示意图:
优点
- 不需要自定义错误类型
- 因为只要实现了
Fail
的都可以转换为Error类型,可以用?操作符 - 同时因为转换方便,面对将来新增依赖带来的新错误类型,也能不加修改地向外传递.
缺点:
- Error要新分配内存,性能有时候没有自定义错误类型好
- 只有downcast才能获取原有的错误类型
适用场景:错误的类型系统不是很重要,重要的是写入日志或者展示给用户。application比较适合,结构化的错误类型不是必须的,重点是能包容各方依赖的错误。其实这个看上去有点像String的升级版,不过支持downcast反向转换。
Custom Fail type
即函数返回的类型为Result<T, ProjectError>, 其中ProjectError是各种错误的枚举,并且实现相关错误Trait,比如failure::Fail、std::error::Error
示意图:
优点:
- 可以枚举可能出现的所有错误类型
- 完全控制每个错误类型的打印格式
- 调用者不用做downcast就可以获得详细的错误信息
缺点:
- 在嵌套错误时,必须显示使用#[cause]指明下层错误,便于derive实现cause。这种形式不一定保证兼容之后所有依赖的错误类型。
- 错误枚举选项(variants)和底层错误是一对一关系。
我不是很理解这个两个缺点的意思,翻译得可能也不准确。
第二点是不是指,如果在只使用?操作符的情况下,你只能为底层错误写一个转换到ProjectError的From trait,只能转换到ProjectError的其中一个枚举,这样语义上可能达不到要求。比如io::Error可以是FileSystemError也可以是NetworkError,在lib层面做一个区分就会更有意义,而这在第4种Error and ErrorKind中能实现,因为使用显示的context来转化。
适用场景:文档说的意思是,这个模式比较适合没有底层错误的错误。如果要组合底层错误,推荐使用后面Error and ErrorKind的模式。
An Error and ErrorKind pair
最健壮的error管理方式,当然维护起来是最麻烦的。
整体思路是2、3的整合。具体是让自定义的ProjectError,承担Error的角色,让所有错误都可以相对快捷地转换到ProjectError类型。
示意图:
ProjecError的主体实际上是一个Context结构,Context左手一个ErrorKind,右手一个failure。ErrorKind就是lib层面自定义的结构化错误,外部代码可以通过对ProjectError调用kind方法来确定错误类型。failure记载导致ErrorKind的下层错误。和Custom Fail Error示意图一样,我用橙色标记了错误的主体,可以看到ErrorKind版本,实际上新增了一层统一的Context抽象,把原本当主体的ErrorKind拿来当一个字段。
优点:
- 兼容性好
- 可以添加信息
- 把底层错误转换到统一的ProjectError
- 不用downcast
缺点:
- 比较复杂,写很多模板代码
- 结构复杂,allocation比较多,不适合性能要求高的应用
- [个人] 不能完全控制错误的Display的方式,Project的Display实现调用Context,Context只会打印ErrorKind的Display,在Debug Trait里则会用换行符连接ErrorKind和cause failure。你只能控制ErrorKind的展示,但是不能控制ErrorKind和cause failure在一块的展示格式。
适用场景:中间层的lib,有众多依赖,且面向生产环境使用。希望提供完整的结构化错误,并且添加足量的上下文信息,可以使用这个模式。
Strings and Custom Fail Type
本质是把ErrorKind换成String、&str。
示意图:
动机说明 & 优点 & 适用场景:
- 想提供除了Error之外的更具语义的ProjectError,比如例子中的EncodeError和DecodeError。同时还不想写复杂的ErrorKind,因为上下文实在太多了,并且上层调用实际上并不是很想错误处理,只想要一个友好的错误说明。
缺点:
- Context\<String>要多出allocation
- [个人] string信息依然散乱在各处,不方便维护
还有更好的选择吗
其实看下来Custom Fail Type的结构是最简单的,Error and ErrorKind在其基础上引入Context而成。
ErrorKind模式不能完全控制展示格式,也正是因为引入的这个Context。如果想完全控制,就得把底层错误写入ErrorKind的字段里。这样就和Context的failure字段重复了,很呆。这个问题在使用context为io Error添加错误时有实际体现(见)。
既然如此,为什么不让context方法直接返回Custom Fail Type呢?这样也能规避Coustom Fail Type和底层错误一对一绑定的尴尬。这个思想就是 crate snafu 的实现依据
1 |
|
经过一个多星期的对比,我目前觉得 snafu 的设计是挺好的,适合需要严谨设计错误的中间lib。事实上,稍微有点追求的application也可以这样搞,因为可以集中修改display的信息,虽然使用String的方案很方便,但是依然存在信息散乱,修改不方便的问题。另外,snafu依赖的也是标准库的error,之后如果有变动,迁移起来会比failure方便。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!