Rust入坑指南:亡羊补牢

如果你已经开始学习Rust,相信你已经体会过Rust编译器的强大。它可以帮助你避免程序中的大部分错误,但是编译器也不是万能的,如果程序写的不恰当,还是会发生错误,让程序崩溃。所以今天我们就来聊一聊Rust中如何处理程序错误,也就是所谓的“亡羊补牢”。

基础概念

在编程中遇到的非正常情况通常可以分为三类:失败、错误、异常。

Rust中用两种方式来消除失败:强大的类型系统和断言。

对于类型系统,熟悉Java的同学应该比较清楚。例如我们给一个接收参数为int的函数传入了字符串类型的变量。这是由编译器帮我们处理的。

rust07-1

关于断言,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<T>、Result<T, E>、线程恐慌(Panic)、程序终止(Abort)。接下来我们对这些方法进行详细介绍。

Option<T>

Option<T>我们在Rust入坑指南:千人千构一文中我们进行过一些介绍,它是一种枚举类型,主要包括两种值:Some(T)和None,Rust也是靠它来避免空指针异常的。

在前文中,我们并没有详细介绍如何从Option<T>中提取出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<T>中提取值的方法,有时我们会觉得每次处理Option<T>都需要先提取,然后再做相应计算这样的操作比较麻烦,那么有没有更加高效的操作呢?答案是肯定的,我从文档中找到了map和and_then这两种方法。

其中map方法和unwrap一样,也是一系列方法,包括map、map_or和map_or_else。map会执行参数中闭包的规则,然后将结果再封为Option<T>并返回。

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<T>我们就先聊到这里,大家只需要记住,它可以用来处理空值,然后能够使用它的一些处理方法就可以了,实在记不住这些方法,也可以在用的时候再去文档中查询。

Result<T, E>

聊完了Option<T>,我们再来看另一种错误处理方法,它也是一个枚举类型,叫做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<T>可以被看作Result<T, ()>。从定义中我们可以看到Result<T, E>有两个变体:Ok(T)和Err(E)。

Result<T, E>用于处理真正意义上的错误,例如,当我们想要打开一个不存在的文件时,或者我们想要将一个非数字的字符串转换为数字时,都会得到一个Err(E)结果。

Result<T, E>的处理方法和Option<T>类似,都可以使用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进行了捕获,结果如图所示。

rust07-2

Rust虽然打印了恐慌信息,但是并没有影响程序的执行,我们的代码println!("{}", 1 + 2);可以正常执行。

总结

至此,Rust处理错误的方法我们已经基本介绍完了,为什么说是基本介绍完了呢?因为还有一些大佬开发了一些第三方库来帮助我们更加方便的处理错误,其中比较有名的有error-chain和failure,这里就不做过多介绍了。

通过本节的学习,相信你的Rust程序一定会变得更加健壮。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容