【GHS】对rust中的std::ioerror的研究

编程开发   © 文章版权由 admin 解释,禁止匿名转载

#楼主# 2020-12-30

在本文中,我们将剖析 Rust 标准库中的std::io::Error类型的实现。对应的代码在:library/std/src/io/error.rs[1]。

你可以把把本文作为:

对标准库某一部分的研究

一份高级错误管理指南

一个美观的 API 设计案例

阅读本文需要对 Rust 的错误处理有基本的了解。

当使用Result设计Error 类型时,主要问题是“错误将会被如何使用?”。通常,会符合下面的情况之一。

错误被代码处理。 用户来检查错误,所以其内部结构应该需要合理的暴露出来。

错误被传播并且展示给用户。用户不会通过超出fmt::Display之外的方式检查错误;所以其内部结构可以被封装。

注意,暴露实现细节和将其封装之间互相牵扯。对于实现第一种情况,一个常见的反模式(译注:即不好的编程实践,详见anti-pattern[2])是定义一个 kitchen-sink[3] 枚举(译注:即把想到的一切错误类型塞到一个枚举中):

pub enum Error {
Tokio(tokio::io::Error),
ConnectionDiscovery {
path: PathBuf,
reason: String,
stderr: String,
},
Deserialize {
source: serde_json::Error,
data: String,
},
...,
Generic(String),
}
但是这种方式存在很多问题。

首先 ,从底层库暴露出的错误会其成为公开 API 的一部分。如果你的依赖库出现重大变更,那么你也需要进行大量修改。

其次,它规定了所有的实现细节。 例如,如果你留意到ConnectionDiscovery很大,对其进行 boxing 将会是一个破坏性的改变。

第三, 它通常隐含着更大的设计问题。Kitchen sink 错误将不同的 failure 模式打包进一种类型。但是,如果 failure 模式区别很大,可能处理起来就不太合理。这看起来更像第二种情况。

对于 kitchen-sink 问题的一个比较奏效的方法是,将错误推送给调用者。 考虑下面的例子:

fn my_function() -> Result {
let thing = dep_function()?;
...
Ok(92)
}

my_function 调用 dep_function,所以MyError应该是可以从DepError转换得来的。下面可能是一种更好的方式

fn my_function(thing: DepThing) -> Result {
...
Ok(92)
}
在这个版本中,调用者可以专注于执行dep_function并处理它的错误。这是用更多的打字(typing)换取更多的类型安全。MyError和DepError现在是不同的类型,调用者可以分别处理他们。如果DepError是MyError的一个变体(variant),那么可能会需要一个运行时的 match。

这种想法的一个极致版本是san-io[4]编程。对于很多来自 I/O 的错误,如果你把所有的 I/O 错误都推给调用者,你就可以略过大多数的错误处理。

尽管使用枚举这种方式很糟糕,但是它确实实现了在第一种情况下将可检查性最大化。

以传播为核心的第二种错误管理,通常使用 boxed trait 对象来处理。一个像Box的类型可以构建于任意的特定具体错误,可以通过Display打印输出,并且可以通过动态地向下转换进行可选的暴露。anyhow[5]就是这种风格的最佳示例。

std::io::Error的这种情况比较有趣,是因为它想同时做到以上两点甚至更多。

这是std,所以封装和面向未来是最重要的。

来自操作系统的 I/O 错误通常可以被处理(例如,EWOULDBLOCK)

对于一门系统编程语言,切实地暴露底层的系统错误是重要的。

io::Error可以作为一个 vocabulary 类型,并且应该能够表示一些非系统错误。例如,Rust 的Path内部可以是 0 字节,对这样的Path在进行打开操作时,应该在进行系统调用之前就返回一个io::Error。

成为第一个回答人

评论

登录后才可发表内容
  • 主题

    15

  • 帖子

    15

  • 关注者

    0

Copyright © 2019 凯特网.   Powered by HYBBS 2.3.4  

Runtime:0.0892s Mem:2064Kb