Rust 语言逆天的错误处理方式
写了几天 Rust 之后,不光被它的 Ownership, Lifetime 折磨的死去活来,还碰上个奇怪的错误处理方式。如果让程序员在 Java, C/C++, Python, Ruby, Scala, Go,甚至是 Lisp 语言之间换着干活,那还都不是难事,但是拉个人去弄 Rust 就会要人命了。
有垃圾回收的语言基本就是想怎么写都成,程序运行时也不会出太大的事,当然性能是个妥协; 像 C/C++ 自己管理内存的语言写出来的程序通过编译也容易,只是执行时很可能会有内存泄漏或地址越界。而选择号称性能与安全兼备的 Rust 语言的话,按照正常思维逻辑写出来的代码能通过编译就是最幸福的事。碰到关于 Ownership, Lifetime 之类的编译问题很多时候当前的 AI 也无解。
所以现在还能用 Java, Python, C# 等写代码是个幸福的事,这种时光应该好好的珍惜。换个角度来想,如果能掌握 Rust 那还怕别的语言吗?
就算能侥幸的应付 Ownership, Lifetime 的问题,Rust 错误处理方式也会让人抓狂,起初大量的用 unwrap() 忽略错误,用多了也会觉得不是一回事。
主流的语言都是采纳 try/catch 方式处理异常方式,异常在栈中向上传播,想在哪一层捕获异常都行,所以才可能在某处集中的处理异常,也让程序结果返回与异常处理得已分离。
有一个视频 什么是正确的错误处理方法【让编程再次伟大#21】介绍了各种编程语言错误处理方式的历史发展,视频博主十分推崇 Rust 的结果错误放 Result 的方式,觉得像 try/catch 那种能够实现关注点分离的方式是便宜了程序员,只有返回 Result<T, E> 的方式才能强制程序员步步小心。那何不让 try/catch 全部抛出 Checked 异常,然而事实是 Spring 框架把许多 Checked 异常转换成了 Unchecked 异常,能程序员处理更方便,且能集中处理异常。
本人先前就对 Playframework 的 F.Either 可返回结果或错误的处理方式颇有微辞,而 Rust 彻底完全采用了 Result<Value, Error> 方式。Rust 有关异常方面只提供 panic! 宏来立即退出程序,像是 Java 异常抛到 main 函数而未处理一样。这方面 Go 有些类似, Go 返回 Tuple (Value, Error) 的方式,也是没有 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() 方法
如果此时读取的是一个不存在的文件,则会产生执行错误
基于此正确的做法通常是用 match 模式匹配分别处理 Ok 和 Err 中的情况
现在的错误信息是
问题处理显示
这样就可以用
如果修改 main() 的返回值为
如果调用方法的 Result 的 Err 与被调用方法返回的 Result Err 部分可兼容的话,就可以用
中问号的意思是如果没有错误,即可被 unwrap() 则 unwrap(), 否则立即返回与调用方法兼容的错误。代码相当于是
如果一个方法调用多个方法时
能用
比如我们写下面的代码
显示 std::io::Error 无法自动转换成 CustomError, 所以在问号处编译出错
这时候可以主动 match 来转换,或者让 CustomError 和 std::io::Error 变得兼容,注意看错误信息里给出了办法,即 `note: `CustomError` needs to implement
给 CustomError 加上实现
现在编译就没问题了。执行后输出为
再次执行的输出就变成了
这样在调用顶层不知道途中发生了什么,对于 Rust 也样,在方法调用链接尽可能的是向上返回 Result<T, E> 或是
但要写成 try! 还不行
提示的错误是
因为在 Rust 2018 edition 中 try 是一个保留关键字,但换成 r#try! 就可以
效果和 read_file("aaa")? 是一样的。
try_catch! 是由不怎么爱 Rust 原生错误处理方式而提供的第三方库,需要安装
虽然看到熟悉的 try/catch 的味道,但是间杂着 Rust 中固有气味,反而散发出某种怪味。
上面执行的错误是
输出
unwrap() 就是一个 match 表达式,有结果则取结果,出错误后最终产生一个 panic!
参考:
永久链接 https://yanbin.blog/rust-weird-error-handling-syntax/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明]
本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
有垃圾回收的语言基本就是想怎么写都成,程序运行时也不会出太大的事,当然性能是个妥协; 像 C/C++ 自己管理内存的语言写出来的程序通过编译也容易,只是执行时很可能会有内存泄漏或地址越界。而选择号称性能与安全兼备的 Rust 语言的话,按照正常思维逻辑写出来的代码能通过编译就是最幸福的事。碰到关于 Ownership, Lifetime 之类的编译问题很多时候当前的 AI 也无解。
所以现在还能用 Java, Python, C# 等写代码是个幸福的事,这种时光应该好好的珍惜。换个角度来想,如果能掌握 Rust 那还怕别的语言吗?
就算能侥幸的应付 Ownership, Lifetime 的问题,Rust 错误处理方式也会让人抓狂,起初大量的用 unwrap() 忽略错误,用多了也会觉得不是一回事。
主流的语言都是采纳 try/catch 方式处理异常方式,异常在栈中向上传播,想在哪一层捕获异常都行,所以才可能在某处集中的处理异常,也让程序结果返回与异常处理得已分离。
有一个视频 什么是正确的错误处理方法【让编程再次伟大#21】介绍了各种编程语言错误处理方式的历史发展,视频博主十分推崇 Rust 的结果错误放 Result 的方式,觉得像 try/catch 那种能够实现关注点分离的方式是便宜了程序员,只有返回 Result<T, E> 的方式才能强制程序员步步小心。那何不让 try/catch 全部抛出 Checked 异常,然而事实是 Spring 框架把许多 Checked 异常转换成了 Unchecked 异常,能程序员处理更方便,且能集中处理异常。
本人先前就对 Playframework 的 F.Either 可返回结果或错误的处理方式颇有微辞,而 Rust 彻底完全采用了 Result<Value, Error> 方式。Rust 有关异常方面只提供 panic! 宏来立即退出程序,像是 Java 异常抛到 main 函数而未处理一样。这方面 Go 有些类似, Go 返回 Tuple (Value, Error) 的方式,也是没有 try/catch, 异常处理方面有 panic, recover 和 differ。
大抵像 Go 和 Rust 恰恰就是对返回结果和异常处理分离有所不悦,才创建出用 Result<Value, Error> 让结果和错误走同一个出口,像是单孔目的鸟类,蛋和屎尿从一个口排出,每次接住结果的时候可能是的蛋,但也可能是排泄物,虽获取到后自己分离。而 try/catch 的方式可以就可以分开来处理,想要获得蛋,只是有或没有,对于排泄物可以完全不管,或者让它直排到臭水沟。
用 Result<Value, Error> 的方式需要每个方法都返回类似的数据类型,如果是不能的 Error 类型之间还需转换或保持兼容。比如说我们不得不写出下面那样的方法返回值
1fn read_file(path: &str) -> std::io::Result<String>
2
3fn read_file1(path: &str) -> result::Result<String, Error>
4
5fn call_library() -> Result<(), Box<dyn std::error::Error>>现在一步步来看 Rust 怎么处理错误的
Result<T, E> 是一个枚举
1enum Result<T, E> {
2 Ok(T),
3 Err(E),
4}1fn main() {
2 let content = read_file("hello.txt").unwrap();
3 println!("file content: {}", content);
4}
5
6fn read_file(path: &str) -> std::io::Result<String> {
7 fs::read_to_string(path)
8}thread 'main' (600254) panicked at src/main.rs:10:43:unwrap() 失败则会产生一个 panic
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }
基于此正确的做法通常是用 match 模式匹配分别处理 Ok 和 Err 中的情况
1fn main() {
2 let content = read_file("hellox.txt");
3
4 match read_file("hellox.txt") {
5 Ok(content) => println!("file content: {}", content),
6 Err(e) => eprintln!("Error reading file: {}", e),
7 }
8}Error reading file: No such file or directory (os error 2)我们看到有些地方用
? 替换 unwrap() 调用,下面的 main() 方法是不行的1fn main() {
2 let content = read_file("hellox.txt")?;
3 println!("file content: {}", content);
4}^ cannot use the `?` operator in a function that returns `()`因为 main() 的返回值是 (), 修改 main() 的返回值类型为
std::io::Result<()>) 或 result::Result<(), io::Error>1fn main() -> result::Result<(), io::Error> {
2 let content = read_file("hellox.txt")?;
3 println!("file content: {}", content);
4 Ok(())
5}? 号了。这里 main() 函数返回值 Result<(), io::Error> 中的 Err 部分 io::Error 正好与 read_file() 返回值的 Err 部分类型一致所以才能用 ? 号。如果修改 main() 的返回值为
result::Result<(), String> 就又不行了1fn main() -> result::Result<(), String> {
2 let content = read_file("hellox.txt")?;
3 println!("file content: {}", content);
4 Ok(())
5}? 处提示^ the trait `From<std::io::Error>` is not implemented for `String`注意,Rust 并没有像支持异常的语言那样严格的错误必须实现了某个基类的类型,而是可以为任何类型,像 C/C++ 那样任何类型皆可 raise。
如果调用方法的 Result 的 Err 与被调用方法返回的 Result Err 部分可兼容的话,就可以用
?。1let content = read_file("hellox.txt")?;1fn main() -> result::Result<(), io::Error> {
2 match read_file("hello.txt") {
3 Ok(content) => Ok(()),
4 Err(e) => Err(e)
5 }
6}1fn main() -> result::Result<(), io::Error> {
2 println!("{}", foo()?);
3 println!("{}", bar()?);
4 println!("{}", baz()?);
5 Ok(())
6}? 号的前提是每个方法返回 Result<T, E> Err 部分必须与 io::Error 兼容,兼容就意味着如果出错时 ? 处必须能自动转换成 io:Error。比如我们写下面的代码
1#[derive(Debug)]
2struct CustomError {
3}
4
5fn main() -> result::Result<(), CustomError> {
6 println!("{}", read_file("aaa")?);
7 Ok(())
8}
9
10fn read_file(path: &str) -> result::Result<String, std::io::Error> {
11 let content = fs::read_to_string(path)?;
12 Ok(content)
13}113 | fn main() -> result::Result<(), CustomError> {
2 | ------------------------------- expected `CustomError` because of this
314 | println!("{}", read_file("aaa")?);
4 | ----------------^ the trait `From<std::io::Error>` is not implemented for `CustomError`
5 | |
6 | this can't be annotated with `?` because it has type `Result<_, std::io::Error>`
7 |
8note: `CustomError` needs to implement `From<std::io::Error>`From<std::io::Error>`给 CustomError 加上实现
1impl From<io::Error> for CustomError {
2 fn from(_err: io::Error) -> Self {
3 CustomError {}
4 }
5}Error: CustomError如果想要获得得原 io::Error 的信息,可以这么实现
1#[derive(Debug)]
2struct CustomError {
3 error: io::Error
4}
5
6impl From<io::Error> for CustomError {
7 fn from(_err: io::Error) -> Self {
8 CustomError {error: _err}
9 }
10}Error: CustomError { error: Os { code: 2, kind: NotFound, message: "No such file or directory" } }
避免使用 unwrap()
好像我们所有做的一切就是在避免使用 unwrap(),函数调用链中途的 unwrap() 操作相当于是在 Java 的某一级方法调用中,catch 异常,打印异常信息并就地终止了运行1try{
2 // do something
3} catch (Exception ex) {
4 ex.printStackTrace();
5 System.exit(1);
6}?, 而非 unwrap().关于 try!, try_catch! 宏
起先在 Rust 也有一个 try! 宏,现标记为1#[deprecated(since = "1.39.0", note = "use the `?` operator instead")]
2#[doc(alias = "?")]
3macro_rules! r#try {1fn main() -> Result<(), io::Error> {
2 println!("{}", try!(read_file("aaa")));
3 Ok(())
4}1error: use of deprecated `try` macro
2 --> src/main.rs:21:20
3 |
421 | println!("{}", try!(read_file("aaa")));
5 | ^^^^^^^^^^^^^^^^^^^^^^
6 |
7 = note: in the 2018 edition `try` is a reserved keyword, and the `try!()` macro is deprecated
8help: you can use the `?` operator instead1fn main() -> Result<(), io::Error> {
2 println!("{}", r#try!(read_file("aaa")));
3 Ok(())
4}try_catch! 是由不怎么爱 Rust 原生错误处理方式而提供的第三方库,需要安装
cargo add rust-try-catch看下如何使用
1fn main() {
2 try_catch! {
3 try {
4 println!("{}", tri!(read_file("aaa")));
5 } catch (exception => CustomError) {
6 println!("Caught a CustomError: {:?}", exception);
7 } catch (exception => io::Error) {
8 println!("Caught an io::Error: {:?}", exception);
9 } catch panic (panic_info) {
10 println!("Caught a panic: {:?}", panic_info);
11 } finally {
12 println!("Execution completed.");
13 }
14 }
15}上面执行的错误是
Caught an io::Error: Os { code: 2, kind: NotFound, message: "No such file or directory" } Execution completed.try_catch! 可以捕获到直接的 panic!
1fn main() {
2 try_catch! {
3 try {
4 panic!("This is a panic for demonstration purposes.");
5 } catch panic (panic_info) {
6 println!("Caught a panic: {:?}", panic_info);
7 }
8 }
9}Caught a panic: Any { .. }最后不妨看一下 Result 的 unwrap() 的实现代码
1imp<T, E> Result<T, E> {
2 ......
3 pub fn unwrap(self) -> T
4 where
5 E: fmt::Debug,
6 {
7 match self {
8 Ok(t) => t,
9 Err(e) => unwrap_failed("called `Result::unwrap()` on an `Err` value", &e),
10 }
11 }
12 ......
13}
14
15fn unwrap_failed(msg: &str, error: &dyn fmt::Debug) -> ! {
16 panic!("{msg}: {error:?}");
17}参考:
永久链接 https://yanbin.blog/rust-weird-error-handling-syntax/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明]
本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。