如果你已经开始学习 Rust, 相信你已经体会过 Rust 编译器的强大. 它可以帮助你避免程序中的大部分错误, 但是编译器也不是万能的, 如果程序写的不恰当, 还是会发生错误, 让程序崩溃. 所以今天我们就来聊一聊 Rust 中如何处理程序错误, 也就是所谓的 "亡羊补牢".
基础概念
在编程中遇到的非正常情况通常可以分为三类: 失败, 错误, 异常.
Rust 中用两种方式来消除失败: 强大的类型系统和断言.
对于类型系统, 熟悉 Java 的同学应该比较清楚. 例如我们给一个接收参数为 int 的函数传入了字符串类型的变量. 这是由编译器帮我们处理的.
关于断言, Rust 支持 6 种断言. 分别是:
- assert!
- assert_eq!
- assert_ne!
- debug_assert!
- debug_assert_eq!
- debug_assert_ne!
从名称我们就可以看出来这 6 种断言, 可以分为两大类, 带 debug 的和不带 debug 的, 它们的区别就是 assert 开头的在调试模式和发布模式下都可以使用, 而 debug 开头的只可以在调试模式下使用. 再来解释每个大类下的三种断言, assert! 是用于断言布尔表达式是否为 true,assert_eq! 用于断言两个表达式是否相等, assert_ne! 用于断言两个表达式是否不相等. 当不符合条件时, 断言会引发线程恐慌(panic!).
Rust 处理异常的方法有 4 种: Option,Result<T, E>, 线程恐慌(Panic), 程序终止(Abort). 接下来我们对这些方法进行详细介绍.
Option
Option 我们在 Rust 入坑指南: 千人千构一文中我们进行过一些介绍, 它是一种枚举类型, 主要包括两种值: Some(T)和 None,Rust 也是靠它来避免空指针异常的.
在前文中, 我们并没有详细介绍如何从 Option 中提取出 T, 其实最基本的, 可以用 match 来提取. 而我也在前文中给你提供了官方文档的链接, 不知道你有没有看. 如果还没来得及看也没有关系, 我把我看到的一些方法分享给你.
这里介绍两种方法, 一种是 expect, 另一种是 unwrap 系列的方法. 我们通过一个例子来感受一下.
- fn main() {
- let a = Some("a");
- let b: Option<&str> = None;
- assert_eq!(a.expect("a is none"), "a");
- assert_eq!(b.expect("b is none"), "b is none"); // 匹配到 None 会引起线程恐慌, 打印的错误是 expect 的参数信息
- assert_eq!(a.unwrap(), "a"); // 如果 a 是 None, 则会引起线程恐慌
- assert_eq!(b.unwrap_or("b"), "b"); // 匹配到 None 时返回指定值
- let k = 10;
- assert_eq!(Some(4).unwrap_or_else(|| 2 * k), 4);// 与 unwrap_or 类似, 只不过参数是 FnOnce() -> T
- assert_eq!(None.unwrap_or_else(|| 2 * k), 20);
- }
这是从 Option 中提取值的方法, 有时我们会觉得每次处理 Option 都需要先提取, 然后再做相应计算这样的操作比较麻烦, 那么有没有更加高效的操作呢? 答案是肯定的, 我从文档中找到了 map 和 and_then 这两种方法.
其中 map 方法和 unwrap 一样, 也是一系列方法, 包括 map,map_or 和 map_or_else.map 会执行参数中闭包的规则, 然后将结果再封为 Option 并返回.
- fn main() {
- let some_str = Some("Hello!");
- let some_str_len = some_str.map(|s| s.len());
- assert_eq!(some_str_len, Some(6));
- }
但是, 如果参数本身返回的结果就是 Option 的话, 处理起来就比较麻烦, 因为每执行一次 map 都会多封装一层, 最后的结果有可能是 Some(Some(Some(...)))这样 N 多层 Some 的嵌套. 这时, 我们就可以用 and_then 来处理了.
利用 and_then 方法, 我们就可以有如下的链式调用:
- fn main() {
- assert_eq!(Some(2).and_then(sq).and_then(sq), Some(16));
- }
- fn sq(x: u32) -> Option<u32> {
- Some(x * x)
- }
关于 Option 我们就先聊到这里, 大家只需要记住, 它可以用来处理空值, 然后能够使用它的一些处理方法就可以了, 实在记不住这些方法, 也可以在用的时候再去文档中查询.
Result<T, E>
聊完了 Option, 我们再来看另一种错误处理方法, 它也是一个枚举类型, 叫做 Result<T, E>, 定义如下:
- #[must_use = "this `Result` may be an `Err` variant, which should be handled"]
- pub enum Result<T, E> {
- Ok(T),
- Err(E),
- }
实际上, Option 可以被看作 Result<T, ()>. 从定义中我们可以看到 Result<T, E > 有两个变体: Ok(T)和 Err(E).
Result<T, E > 用于处理真正意义上的错误, 例如, 当我们想要打开一个不存在的文件时, 或者我们想要将一个非数字的字符串转换为数字时, 都会得到一个 Err(E)结果.
Result<T, E > 的处理方法和 Option 类似, 都可以使用 unwrap 和 expect 方法, 也可以使用 map 和 and_then 方法, 并且用法也都类似, 这里就不再赘述了. 具体的方法使用细节可以自行查看官方文档.
这里我们来看一下如何处理不同类型的错误.
Rust 在 std::io 模块定义了统一的错误类型 Error, 因此我们在处理时可以分别匹配不同的错误类型.
- use std::fs::File;
- use std::io::ErrorKind;
- fn main() {
- let f = File::open("hello.txt");
- let f = match f {
- Ok(file) => file,
- Err(error) => match error.kind() {
- ErrorKind::NotFound => match File::create("hello.txt") {
- Ok(fc) => fc,
- Err(e) => panic!("Problem creating the file: {:?}", e),
- },
- ErrorKind::PermissionDenied => panic!("Permission Denied!"),
- other_error => panic!("Problem opening the file: {:?}", other_error),
- },
- };
- }
在处理 Result<T, E > 时, 我们还有一种处理方法, 就是 try! 宏. 它会使代码变得非常精简, 但是在发生错误时, 会将错误返回, 传播到外部调用函数中, 所以我们在使用之前要考虑清楚是否需要传播错误.
对于上面的代码, 使用 try! 宏就会非常精简.
- use std::fs::File;
- fn main() {
- let f = try!(File::open("hello.txt"));
- }
try! 使用起来虽然简单, 但也有一定的问题. 像我们刚才提到的传播错误, 再就是有可能出现多层嵌套的情况. 因此 Rust 引入了另一个语法糖来代替 try!. 它就是问号操作符 "?".
- use std::fs::File;
- use std::io;
- use std::io::Read;
- fn main() {
- read_username_from_file();
- }
- fn read_username_from_file() -> Result<String, io::Error> {
- let mut f = File::open("hello.txt")?;
- let mut s = String::new();
- f.read_to_string(&mut s)?;
- Ok(s)
- }
问号操作符必须在处理错误的代码后面, 这样的代码看起来更加优雅.
恐慌(Panic)
我们从最开始就聊到线程恐慌, 那道理什么是恐慌呢?
在 Rust 中, 无法处理的错误就会造成线程恐慌, 手动执行 panic! 宏时也会造成恐慌. 当程序执行 panic! 宏时, 会打印相应的错误信息, 同时清理堆栈并退出. 但是栈回退和清理会花费大量的时间, 如果你想要立即终止程序, 可以在 Cargo.toml 文件中 [profile] 区域中增加 panic = 'abort', 这样当发生恐慌时, 程序会直接退出而不清理堆栈, 内存空间都由操作系统来进行回收.
程序报错时, 如果你想要查看完整的错误栈信息, 可以通过设置环境变量 RUST_BACKTRACE=1 的方式来实现.
如果程序发生恐慌, 我们前面所说的 Result<T, E > 就不能使用了, Rust 为我们提供了 catch_unwind 方法来捕获恐慌.
- use std::panic;
- fn main() {
- let result = panic::catch_unwind(|| {panic!("crash and burn")});
- assert!(result.is_err());
- println!("{}", 1 + 2);
- }
在上面这段代码中, 我们手动执行一个 panic 宏, 正常情况下, 程序会在第一行退出, 并不会执行后面的代码. 而这里我们用了 catch_unwind 方法对 panic 进行了捕获, 结果如图所示.
Rust 虽然打印了恐慌信息, 但是并没有影响程序的执行, 我们的代码 println!("{}", 1 + 2); 可以正常执行.
总结
至此, Rust 处理错误的方法我们已经基本介绍完了, 为什么说是基本介绍完了呢? 因为还有一些大佬开发了一些第三方库来帮助我们更加方便的处理错误, 其中比较有名的有 error-chain 和 failure, 这里就不做过多介绍了.
通过本节的学习, 相信你的 Rust 程序一定会变得更加健壮.
来源: https://www.cnblogs.com/Jackeyzhe/p/12128741.html