如果你已经开始学习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程序一定会变得更加健壮。

Rust入坑指南:亡羊补牢的更多相关文章

  1. Rust入坑指南:核心概念

    如果说前面的坑我们一直在用小铲子挖的话,那么今天的坑就是用挖掘机挖的. 今天要介绍的是Rust的一个核心概念:Ownership.全文将分为什么是Ownership以及Ownership的传递类型两部 ...

  2. Rust入坑指南:鳞次栉比

    很久没有挖Rust的坑啦,今天来挖一些排列整齐的坑.没错,就是要介绍一些集合类型的数据类型."鳞次栉比"这个标题是不是显得很有文化? 在Rust入坑指南:常规套路一文中我们已经介绍 ...

  3. Rust入坑指南:朝生暮死

    今天想和大家一起把我们之前挖的坑再刨深一些.在Java中,一个对象能存活多久全靠JVM来决定,程序员并不需要去关心对象的生命周期,但是在Rust中就大不相同,一个对象从生到死我们都需要掌握的很清楚. ...

  4. Rust入坑指南:齐头并进(上)

    我们知道,如今CPU的计算能力已经非常强大,其速度比内存要高出许多个数量级.为了充分利用CPU资源,多数编程语言都提供了并发编程的能力,Rust也不例外. 聊到并发,就离不开多进程和多线程这两个概念. ...

  5. Rust入坑指南:常规套路

    搭建好了开发环境之后,就算是正式跳进Rust的坑了,今天我就要开始继续向下挖了. 由于我们初来乍到 ,对Rust还不熟悉,所以我决定先走一遍常规套路. 变不变的变量 学习一门语言第一个要了解的当然就是 ...

  6. Rust入坑指南:坑主驾到

    欢迎大家和我一起入坑Rust,以后我就是坑主,我主要负责在前面挖坑,各位可以在上面看,有手痒的也可以和我一起挖.这个坑到底有多深?我也不知道,我是抱着有多深就挖多深的心态来的,下面我先跳了,各位请随意 ...

  7. Rust入坑指南:千人千构

    坑越来越深了,在坑里的同学让我看到你们的双手! 前面我们聊过了Rust最基本的几种数据类型.不知道你还记不记得,如果不记得可以先复习一下.上一个坑挖好以后,有同学私信我说坑太深了,下来的时候差点崴了脚 ...

  8. Rust入坑指南:有条不紊

    随着我们的坑越来越多,越来越大,我们必须要对各种坑进行管理了.Rust为我们提供了一套坑务管理系统,方便大家有条不紊的寻找.管理.填埋自己的各种坑. Rust提供给我们一些管理代码的特性: Packa ...

  9. Rust入坑指南:海纳百川

    今天来聊Rust中两个重要的概念:泛型和trait.很多编程语言都支持泛型,Rust也不例外,相信大家对泛型也都比较熟悉,它可以表示任意一种数据类型.trait同样不是Rust所特有的特性,它借鉴于H ...

随机推荐

  1. Spring Data JPA 查询结果返回至自定义实体

    本人在实际工作中使用Spring Data Jpa框架时,一般查询结果只返回对应的Entity实体.但有时根据实际业务,需要进行一些较复杂的查询,比较棘手.虽然在框架上我们可以使用@Query注解执行 ...

  2. python if 选择结构

  3. 微服务开源生态报告 No.1

    从关注开源,到使用开源,再到参与开源贡献,越来越多的国内开发者通过开源技术来构建业务. 截止目前,Arthas / Dubbo / ChaosBalde / Nacos / RocketMQ / Se ...

  4. 洛谷P2820 局域网

    #include<bits/stdc++.h> using namespace std; ; ; int n,k,sum,tot; struct node{ int cnt,fa; }f[ ...

  5. @noi.ac - 170@ 数数

    目录 @description@ @solution@ @accepted code@ @details@ @description@ 求有多少对 1 ∼ n 的排列 (a, b) 满足 \(m \l ...

  6. 学习微信小程序

    1.从小程序指南文档开始看起:小程序指南 2.开发者工具下载:小程序开发工具

  7. xml path 列转行实例

    SQL Server2005提供了一个新查询语法——For XML PATH(''),这个语法有什么用呢?想象一下这样一个查询需求:有两个表,班级表A.学生表B,要查询一个班级里有哪些学生?针对这个需 ...

  8. functiils.lru_cache缩短递归时间

    力扣上看到一道题: 假设你正在爬楼梯.需要 n 阶你才能到达楼顶. 每次你可以爬 1 或 2 个台阶.你有多少种不同的方法可以爬到楼顶呢? 注意:给定 n 是一个正整数. 使用普通递归解决,超出时间限 ...

  9. H3C PPP显示与调试

  10. servicemix-3.2.1 部署异常

    <jbi-task xmlns="http://java.sun.com/xml/ns/jbi/management-message" version="1.0&q ...