Rust 语言逆天的错误处理方式

写了几天 Rust 之后,不光被它的 Ownership, Lifetime 折磨的死去活来,还碰上个奇怪的错误处理方式。如果让程序员在 Java, C/C++, Python, Ruby, Scala, Go,甚至是 Lisp 语言之间换着学,那还都不是难事,但是拉个人去弄 Rust 就要命了。

有垃圾回收的语言基本就是想怎么写都成,程序运行时也不会出太大的事; 像 C/C++  自己管理内存的语言写出来的程序通过编译也容易,只是执行时会有内存泄漏或地址越界。而选择号称性能与安全兼备的 Rust 语言的话,按照正常思维逻辑写出来的代码能通过编译就是最幸福的事。碰到关于 Ownership, Lifetime 之类的编译问题基本当前的 AI 也无解。

所以现在还能快乐的用 Java, Python 写代码的时光应该好好的去珍惜。

就算能侥幸的应付 Ownership, Lifetime 的问题,Rust 错误处理方式也会让人抓狂,起初大量的用 unwrap() 忽略错误,用多了也会觉得不是一回事。

主流的语言都是采纳 try/catch 方式处理异常方式,异常在栈中向上传播,想在哪一层捕获异常都行,所以才可能在某处集中的处理异常,也让程序结果返回与异常处理得已分离。

本人先前就对 Playframework 的 F.Either 可返回结果或错误的处理方式颇有微辞,而 Rust 彻底完全采用了 Result<Value, Error> 方式。Rust 有关异常方面只提供 panic! 宏来立即退出程序,像是 Java 异常抛到 main 函数而未处理一样。这方面 Go 有些类似,也是没有 try/catch, 异常处理方面有 panic, recover 和 differ。

大抵像 Go 和 Rust 恰恰就是对返回结果和异常处理分离有所不悦,才创建出用 Result<Value, Error> 让结果和错误走同一个出口,像是单孔目的鸟类,蛋和屎尿从一个口排出,每次接住的时候可能是要的蛋,但也可能是排泄物,虽获取到后自己分离。而 try/catch 的方式可以只获得蛋,有或没有,对于排泄物可以完全不管。

用 Result<Value, Error> 的方式需要每个方法都返回类似的数据类型,如果是不能的 Error 类型之间还需转换或保持兼容。比如说我们不得不写出下面那样的方法返回值

上面  Result<String> 是 Result<String, Error>  的类型别名,如果像 std:error:Error 是一个 trait, 在编译器无法确定内存占用大小,所以不得不用 Box<dyn std::error:Error>> 的形式。

现在一步步来看 Rust 怎么处理错误的

Result<T, E> 是一个枚举

如果要从 Result 中得到值可以调用  unwrap() 方法

如果此时读取的是一个不存在的文件,则会产生执行错误

thread 'main' (600254) panicked at src/main.rs:10:43:
called Result::unwrap() on an Err value: Os { code: 2, kind: NotFound, message: "No such file or directory" }

unwrap() 失败则会产生一个 panic

基于此正确的做法通常是用 match 模式匹配分别处理 Ok 和 Err 中的情况

现在的错误信息是

Error reading file: No such file or directory (os error 2)

我们看到有些地方用 ? 替换 unwrap() 调用,下面的 main() 方法是不行的

问题处理显示

^ cannot use the ? operator in a function that returns ()

因为 main() 的返回值是 (), 修改 main() 的返回值类型为  std::io::Result<()>)result::Result<(), io::Error>

这样就可以用 ?  号了。这里 main() 函数返回值 Result<(), io::Error> 中的 Err 部分 io::Error 正好与 read_file() 返回值的 Err 部分类型一致所以才能用 ? 号。

如果修改 main() 的返回值为 result::Result<(), String> 就又不行了

? 处提示

^ the trait From<std::io::Error> is not implemented for String

注意,Rust 并没有像支持异常的语言那样严格的错误必须实现了某个基类的类型,而是可以为任何类型,像 C/C++ 那样任何类型皆可 raise。

如果调用方法的 Result 的 Err 与被调用方法返回的 Result Err 部分可兼容的话,就可以用 ?

中问号的意思是如果没有错误,即可被 unwrap() 则  unwrap(), 否则立即返回与调用方法兼容的错误。代码相当于是

如果一个方法调用多个方法时

能用 ? 号的前提是每个方法返回 Result<T, E> Err 部分必须与 io::Error 兼容,兼容就意味着如果出错时  ? 处必须能自动转换成 io:Error。

比如我们写下面的代码

显示 std::io::Error 无法自动转换成 CustomError,  所以在问号处编译出错

这时候可以主动 match 来转换,或者让 CustomError 和 std::io::Error 变得兼容,注意看错误信息里给出了办法,即 note: CustomError needs to implement From<std::io::Error>

给  CustomError 加上实现

现在编译就没问题了。执行后输出为

Error: CustomError

如果想要获得得原 io::Error 的信息,可以这么实现

再次执行的输出就变成了

Error: CustomError { error: Os { code: 2, kind: NotFound, message: "No such file or directory" } }

避免使用 unwrap()

好像我们所有做的一切就是在避免使用  unwrap(),函数调用链中途的 unwrap() 操作相当于是在 Java 的某一级方法调用中,catch 异常,打印异常信息并就地终止了运行

这样在调用顶层不知道途中发生了什么,对于 Rust 也样,在方法调用链接尽可能的是向上返回  Result<T, E> 或是 ?, 而非 unwrap().

关于 try!, try_catch! 宏

起先在 Rust 也有一个  try! 宏,现标记为

但要写成 try! 还不行

提示的错误是

因为在 Rust 2018 edition 中 try 是一个保留关键字,但换成 r#try! 就可以

效果和 read_file("aaa")? 是一样的。

try_catch! 是由不怎么爱 Rust 原生错误处理方式而提供的第三方库,需要安装

cargo add rust-try-catch

看下如何使用

虽然看到熟悉的 try/catch 的味道,但是间杂着 Rust 中固有气味,反而散发出某种怪味。

上面执行的错误是

Caught an io::Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }
Execution completed.

try_catch! 可以捕获到直接的 panic!

输出

Caught a panic: Any { .. }

最后不妨看一下 Result 的 unwrap() 的实现代码

unwrap() 就是一个 match 表达式,有结果则取结果,出错误后最终产生一个  panic!

参考:

  1. 细说Rust错误处理

本文链接 https://yanbin.blog/rust-weird-error-handling-syntax/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

guest

0 Comments
Inline Feedbacks
View all comments