【Rust 学习记录】9. 错误处理
前言
Rust 里的错误主要分为两种:1. 不可恢复错误:主要指的就是程序Bug之类的用户不可见的错误,例如尝试访问超过数组长度的下标;2. 可恢复错误,例如文件没找到等,可以提示用户再次查找。Rust 对这两种错误进行了区分,并针对不同的场景提供了许多的特性来处理
不可恢复错误与Panic!
Panic!宏
介绍
Panic!
宏是专门用于处理某个错误被检测到,而程序员不知道该怎么处理的情景。Panic!
宏会首先打印一段错误提示信息,然后沿着调用的栈反向遍历,清理等待执行的指令以及它们的数据,清理完毕后退出程序。
提示
PS: Rust程序打包时会默认附带很多信息来支持Panic!
的栈展开清理操作,如果你不需要 Rust 自己清理,可以接受把内存交由操作系统来回收,并且需要打包的二进制包体尽可能小的话,可以在Cargo.toml
的[profile]
区域添加panic = 'abort'
来讲panic的默认行为从展开切换为直接终止。例如
1 | [profile.release] |
使用
1 | fn main() { |
运行这段代码,会得到报错
1 | thread 'main' panicked at 'crash and burn', src\main.rs:2:5note: run with RUST_BACKTRACE=1 environment variable to display a backtrace |
第一句很简单,输出了我们的报错信息,并告诉了我们panic
的位置在main.rs
第2行第5个字符。
第二句则提示我们可以用环境变量RUST_BACKTRACE=1
显示回溯信息,是什么意思呢?我们可以试一下。设置环境变量后再运行
1 | // windows系统下,powersell环境的命令 |
我们可以看到新的报错信息
1 | stack backtrace: |
可以看到这就是我们panic
栈展开的时候的具体信息,首先是panic
函数,再是panic
标准化输出,然后是我们自己的文件.\src\main.rs
的main
函数,最后我也不清楚,可能是main
函数入口吧。
我们就可以根据这个回溯信息,一个一个查找到错误发生的地方,进行排除(感觉用的不会很多?)
另外,后面我们还得运行代码,如果不想看到这么一长串,记得把这个环境变量设回0
可恢复错误与Result
Result枚举类型
简单使用
其实我们在之前就已经使用过了Result
了,在编写随机数字时,我们在处理用户输入的时候就用了expect
函数,那时候我们就简单的提到了,Result
是一个枚举类型,包含Ok
和Err
两个变体。它的定义是这样的:
1 | enum Result<T,E>{ |
其中T,E
是两个泛型参数,下一章就会讨论到泛型。总之就是1. T
包含了Ok
里的值,跟随着程序执行成功时返回对应的值;2. E
包含了错误类型,跟随着执行失败的时候返回
当一个可能会运行失败的函数,可以将Result
作为返回结果,例如标准库里的打开文件操作
1 | use std::fs::File; |
File::open
函数回返回一个Result
类型,所以我们需要用match
对f
进行额外的处理,取出Result
里的值,才能正确的获得打开文件,这迫使程序员必须用match枚举所有可能性,不然就无法正常编译~
处理不同错误
文件打开可能有多种错误,可能是文件不存在,可能是没有文件的读权限,那我们怎么从Err
里分辨多种错误呢?
1 | use std::fs::File; |
这段代码多了不少东西,我们一个一个来说
- 通过
use
引入了io
标准库的标准io
相关错误类型ErrorKind
error
错误类型的值可以通过.kind
函数获取它的错误类型,错误类型也是一个枚举类型- 所以可以通过 match 匹配错误类型,我们针对找不到文件的类型作了特殊处理——创建一个文件(创建文件的同时也要处理创建是否成功的Result类型),对于其他类型不知道怎么处理,就调用
panic!
可以看见,写一段 rust 代码突然变得繁琐起来了,本来我们只要几行代码就搞定的东西,Rust 却逼我们写了一堆 match 来处理报错,可读性也损失了不少。
书上提到了 Rust 提供了一个闭包的特性解决这个问题,简化代码,增加错误处理的可读性,以下是一个代码示例。但具体在后面讲解闭包的时候再作解释。
1 | use std::fs::File; |
快捷处理方式
每次都写 match 和 panic! 来处理实在太麻烦了,Rust 当然也提供了一些快捷方式——expect
和unwrap
unwrap
unwarp
可以在返回的是 Ok 时直接返回值,是 Err 时直接自动帮你触发 panic! 报错。
1 | use std::fs::File; |
没有文件时,会直接触发报错
1 | thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "系统找不到指定的文件。" }' |
expect
expect
的效果和unwarp
是差不多的,不同点在于expect
可以让程序员自己指定一个报错的提示信息
1 | thread 'main' panicked at 'Failed to open hello2.txt: Os { code: 2, kind: NotFound, message: "系统找不到指定的文件。" }', src\main.rs:4:38 |
可以看见 paniked at xxx 的地方发生了变化
错误传播
上面提到了可能执行失败的函数会返回一个错误类型,那我们自己写的函数怎么返回呢?以下是一个例子
1 | use std::fs::File; |
这里我们定义了一个函数,返回类型是 Result 枚举,其中两个关于Ok和Err的泛型参数被我们规定为了字符串和标准io错误(这里用标准io错误是因为我们函数里相关的错误都是io错误,当然你可以自己定义其他错误,作一个额外处理然后返回)。
函数里首先对 hello.txt 文件进行读取,读取成功的话取出 Ok 里的文件句柄 file,失败的话则用 return
关键字提前结束函数,返回 Err 类型,然后读取文件内容,存到字符串里,同时也要处理是否读取成功,最后一个 match 表达式不写分号即可直接返回值,不用 return。
PS: 这里有个小细节,f 必须是可变的才能读取内容。为什么呢?我只读文件没有修改文件内容啊。因为这里读取到的 f 只是一个句柄,而我们真正读取内容的时候,需要修改偏移量什么的吧。因此需要可变。
?运算符
因为错误传播太常见了,所以这一块有着一点语法糖——?
运算符
我们直接看一段例子
1 | fn my_function() -> Result<String, io::Error>{ |
是不是感觉瞬间干净了不少
?
运算符的工作和我们之前的match是差不多的,首先,它只能用于Reult类型的后面,且只能用于返回值的Result类型的函数。在值为Ok的时候,它会把Ok的值返回作为这个表达式的结果。在值为Err的时候,它会调用return把错误类型作为整个程序的返回。
最后我们手动构建了一个Result的Ok变体用于运行成功的返回。
但?
运算符也和match表达式有一点点不同。那就是它对于错误类型,也就是Err(e)
里的e
,会偷偷调用一次from
函数,把错误类型转换成我们函数所返回的错误类型,这对于我们有不同类型的错误,但函数返回的类型只有一种时很有用,如果是match还需要我们手动进行额外处理。
当然,我们还能把代码写的更短,在实际项目中,给文件句柄赋予一个 f 变量其实也是多余的
1 | fn my_function() -> Result<String, io::Error>{ |
各种报错方法的使用场景
关于panic!和Result
调用 panic! 代表程序已经无法从这个错误中恢复了,所以选择自行了解。所以当你觉得在某种情况下无论如何也恢复不了,就可以代替使用这个函数的人直接 panic!,不然大多时候都可以选择返回一个 Result,交由使用者自己决定是不是 panic!
当然,还有另外一种情况,那就是用户的操作违反了你所编写的一些程序的“约定”,且这个非法操作会破坏你代码的一些原有行为,难以恢复,又或者是可能会触及到你代码的一些安全漏洞,这时候就可以选择直接 panic! 以终止用户的非法行为。例如数组访问越界,防止用户因为数组访问越界,访问到不属于这个数组的内存数据,以此造成一些安全问题的时候,我们通常会直接 panic!
关于expect和unwrap
对于早期开发和测试
使用 expect 和 unwrap 处理错误固然方便,但也代表我们失去了处理不同错误的机会,因为它会帮助我们直接把程序 panic! 掉。
所以一般情况下,我们都会把 expect 和 unwrap 当成是一个占位符,告诉后面,这里有个错误需要处理,但在开发的早期和需要测试的时候,我们为了方便会选择直接把程序 panic! 掉,既提高了开发效率,也可以方便测试,毕竟程序都直接崩溃了,这里的bug你总不能忽视了吧?快修bug去。
当然在后期交付用户的时候,为了用户友好性,当然不能一点小错误就直接 panic!,这时候我们就可以一个一个的查询之前留下的 expect 和 unwrap,去处理更细致的报错。
对于你确定不会发生报错的时候
在某些时候,你确定100%不可能会出现 Err 变体,那当然也能直接用 unwrap 节省代码量。例如
1 | use std::net::IpAddr; |
127.0.0.1
总不可能是个非法IP地址了吧?这还要我去处理各种报错就有点不合理了。
以上,就是错误处理的全部内容了,大致总结一下就是
- panic! 和 Result——硬处理和软处理的方式
- Result处理的优化——expect, unwrap 和 ?运算符
- 错误类型——error.kind()
- 一些基本的错误处理场景原则
第10章开始就是 trait,泛型和生命周期了。
- 标题: 【Rust 学习记录】9. 错误处理
- 作者: TwoSix
- 创建于 : 2023-04-06 22:23:58
- 更新于 : 2024-07-04 23:52:28
- 链接: https://twosix.page/2023/04/06/【Rust-学习记录】9-错误处理/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。