Rust入坑指南:亡羊补牢

如果你已经开始学习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

时间: 2024-10-13 11:32:12

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

Rust入坑指南:坑主驾到

欢迎大家和我一起入坑Rust,以后我就是坑主,我主要负责在前面挖坑,各位可以在上面看,有手痒的也可以和我一起挖.这个坑到底有多深?我也不知道,我是抱着有多深就挖多深的心态来的,下面我先跳了,各位请随意. Rust简介 众所周知,在编程语言中,更易读的高级语言和控制底层资源的低级语言是一对矛盾体.Rust想要挑战这一现状,它尝试为开发者提供更好的体验的同时给予开发者控制底层细节的权限(比如内存使用). 低级语言在开发过程中很容易出现各种细微的错误,它们难以发现但是可能影响巨大.其他大部分低级语言只

Rust入坑指南:海纳百川

今天来聊Rust中两个重要的概念:泛型和trait.很多编程语言都支持泛型,Rust也不例外,相信大家对泛型也都比较熟悉,它可以表示任意一种数据类型.trait同样不是Rust所特有的特性,它借鉴于Haskell中的Typeclass.简单来讲,Rust中的trait就是对类型行为的抽象,你可以把它理解为Java中的接口. 泛型 在前面的文章中,我们其实已经提及了一些泛型类型.例如Option.Vec和Result<T, E>.泛型可以在函数.数据结构.Enum和方法中进行定义.在Rust中,

Rust入坑指南:步步为营

俗话说:"测试写得好,奖金少不了." 有经验的开发人员通常会通过单元测试来保证代码基本逻辑的正确性.如果你是一名新手开发者,并且还没体会到单元测试的好处,那么建议你先读一下我之前的一篇文章代码洁癖系列(七):单元测试的地位. 写单元测试一般需要三个步骤: 准备测试用例,测试用例要能覆盖尽可能多的代码 执行需要测试的代码 判断结果,是否是你希望得到的结果 了解了这些以后,我们就来看看在Rust中应该怎么写单元测试. 首先我们建立一个library项目 $ cargo new adder

Rust入坑指南:齐头并进(下)

前文中我们聊了Rust如何管理线程以及如何利用Rust中的锁进行编程.今天我们继续学习并发编程, 原子类型 许多编程语言都会提供原子类型,Rust也不例外,在前文中我们聊了Rust中锁的使用,有了锁,就要小心死锁的问题,Rust虽然声称是安全并发,但是仍然无法帮助我们解决死锁的问题.原子类型就是编程语言为我们提供的无锁并发编程的最佳手段.熟悉Java的同学应该知道,Java的编译器并不能保证代码的执行顺序,编译器会对我们的代码的执行顺序进行优化,这一操作成为指令重排.而Rust的多线程内存模型不

C语言入坑指南-被遗忘的初始化

前言 什么是初始化?为什么要初始化?静态变量和局部变量的初始化又有什么区别?实际应用中应该怎么做?本文将一一回答这些问题. 什么是初始化 初始化指的是对数据对象或者变量赋予初始值.例如: int value = 8; //声明整型变量并初始化为8int arr[] = {1,2,3}; //声明整型数组arr,并初始化其值为1,2,3 为什么要初始化 我们来看一个示例程序.test0.c程序清单如下: #include <stdio.h>#include <stdlib.h>int

eclipse中导入外部包却无法查看对应源码或Javadoc的 入坑指南

eclipse中导入外部包却无法查看对应源码或Javadoc的 入坑指南 出现这个错误的原因是,你虽然导入了.jar包,但没有配置对应的Javadoc或源码路径,所以在编辑器中无法查看源 码和对应API.接下来我们一起解决这个问题... 在项目名称上右击→ 新建→ 文件夹→ 文件名写lib→ 点击完成 然后把你下载的jar包,复制黏贴到这个lib文件夹 右击lib中的源码包→ 构建路径→ 添加至构建路径,自动生成一个"引用的库" 右击 "引用的库" 中的jar包→

猿说摄影(上)--入坑指南

最近师弟师妹们以及复读的童鞋临近毕业,有的想买相机拍拍毕业照,记录一下旅行毕业游之类的.五一放假,咱就先不聊技术,聊一下摄影,不过摄影也是一个技术活,而且烧钱.摄影穷三代,单反毁一生.相机贵吗?贵,但贵的不只是相机,还有镜头.为什么这么说呢?大家也知道,单反和微单都是可以更换镜头的.一旦入坑,除了买相机同时买的套头(标准变焦镜头)之外,你很可能会接下来陆陆续续地买其它镜头→_→想拍漂亮的人物,你需要大光圈的定焦镜头:想拍壮阔的风景,你需要广角镜头:想拍飞禽走兽,你需要长焦镜头:你可能还要拍点小花

Docker入坑指南之RUN

总有一些场景,我们需要自己制作一个镜像,可以快速还原环境,又不想被其他因素干扰镜像的纯净,这个时候,就可以选择Docker了,启动便捷,镜像还原很快捷,除了上手不容易. 最近入坑研究了一番,小有心得,故写一篇杂文,记录自己的踩坑经历. 安装Docker的过程可以参考其他前辈的文章,不再赘述,从实战角度说,如何构建一个自用的Docker镜像. 首选说一下Docker的几个名词,仓库是管理镜像的,容器是镜像启动后的,镜像就是最干净的环境,镜像启动之后变成容器. docker的run是启动镜像的介质,

Kotlin快速入坑指南(干货型文档)

<p style="text-align:center;color:#42A5F5;font-size:2em;font-weight: bold;">前言 即使每天10点下班,其实需求很多,我也要用这腐朽的声带喊出:我要学习,我要写文章!! 又是一篇Kotlin的文章,为啥...还不是因为工作需要.毫无疑问,最好的学习方式是通过官方文档去学习.不过个人觉得官方文档多多少少有一些不够高效. 中文官方文档 因此这篇是从我学习的个人视角以文档的形式去输出Kotlin语言基础的学