std::io::Error的首要问题是,当一个文件系统操作失败时,你不知道它失败的路径。这是可以理解的——Rust 是一门系统语言,所以它不应该比 OS 原生提供的东西增加多少内容。OS 返回的是一个整数返回代码,而将其与一个分配在堆上的 PathBuf 耦合在一起可能是一个不可接受的开销。
我很惊讶地发现,事实上,std 在每一个与路径相关的系统调用中都会进行分配。
它需要以某种形式存在。OS API 需要在字符串的结尾有一个零字节。但我想知道对短路径使用栈分配的缓冲区是否有意义。可能不会_路径通常不会那么短,而且现代分配器能有效地处理瞬时分配。
我不知道有什么好的解决方案。一个选择是在编译时(一旦我们得到能觉察std的 cargo)或运行时(像 RUST_BACKTRACE 那样)添加开关,所有路径相关的 IO 错误都在堆上分配。一个类似的问题是 io::Error 不支持 backtrace。
另一个问题是,std::io::Error的效率不高。
它的大小相当大:
assert_eq!(size_of::(), 2 * size_of::());
对于自定义情况,它会产生二次的间接性和分配:
enum Repr {
Os(i32),
Simple(ErrorKind),
// First Box :|
Custom(Box),
}
struct Custom {
kind: ErrorKind,
// Second Box :(
error: Box,
}
我认为现在我们可以修正这个问题!
首先, 我们可以通过使用一个比较轻的 trait 对象来避免二次间接性,按照failure[9]或者anyhow[10]的方式。现在,有了GlobalAlloc[11], 它是个相对直观的实现。
其次,我们可以根据指针是对齐的这一事实,将OS和Simple变量都藏进具有最低有效位的usize。我认为我们甚至可以发挥想象,使用第二个最低有效位,把第一个有效位留作他用。这样一来,即使是像 io::Result这样的东西也可以是指针大小的!
本篇文章到此结束。下一次你要为你的库设计一个错误类型的时候,花点时间看看 std::io::Error 的源码[12],你可能会发现一些值得借鉴的东西。
益智问题
看看这个实现中的这一行:Repr::Custom(c) => c.error.fmt(fmt)
impl fmt::Display for Error {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.repr {
Repr::Os(code) => {
let detail = sys::os::error_string(*code);
write!(fmt, "{} (os error {})", detail, code)
}
Repr::Simple(kind) => write!(fmt, "{}", kind.as_str()),
Repr::Custom(c) => c.error.fmt(fmt),
}
}
}
