Rust强制我们承认出错的可能性,并在代码中主动采取行动,让程序更加健壮
Rust将错误分成 可恢复 和 不可恢复,大部分语言不会进行区分,而是统一用异常来处理,Rust没有异常。可恢复对应Result
我觉得C/C++中对异常处理的支持很有问题,不使用RAII的话,内存管理碰到异常或错误就很容易资源泄漏。有的语言像C++/Java支持用try-catch-finally组合拳,但是这种机制对异常安全性的保证很片面。某些函数利用返回值表示异常,这也可能会导致类型不安全的问题,比如我希望函数返回的int指针不是空,结果返回空指针后我自己也忘了处理,稀里糊涂地做出非法操作。此外对于异常的测试往往是不够的,因为我们很多时候无法穷尽所有异常的可能性来对代码进行检测。类型安全,异常安全很重要。
1、panic! 和不可恢复的错误
出现panic!时,选择程序的动作
默认行为:展开unwinding
- 回溯栈
- 清理回溯过程中每一个函数的数据
- 清理过程有很多工作
选择直接终止abort
- 不清理数据,直接退出
- os来清理程序使用的内存
如果希望项目的最终二进制文件越小越好,可以在 Cargo.toml 的 [profile] 部分添加 panic=’abort’
使用panic!的backtrace
bt是什么
是一个执行到当前位置时,所有被调用的函数的列表
- 用来找代码中出问题的地方
- 使用bt的信息需要debug模式,默认就是debug
开启bt需要指定RUST_BACKTRACE,具体是在命令行输入如下指令
RUST_BACKTRACE=1 cargo run
bt阅读约定
从头开始读,刚开始是问题的发源地
- 当前行向上是当前行代码调用的代码;当前行向下是调用当前行代码的代码
- 中间可能包括Rust核心代码、标准库代码、用到的crate代码等,有时候我们对这些没兴趣
2、Result与可恢复的错误
以打开文件为例
- File::open 返回 io::Error
- 其中io::Error有一个kind方法可获取到io::ErrorKind,它是标准库提供的枚举,它的成员对应的io操作可能导致不同的错误类型,我们感兴趣的类型是ErrorKind::NotFound
打开文件f后,需要match打开文件的结果
- 处理找不到文件的错误时,需要创建文件,并match创建文件可能的问题
- 最终成功的结果都是返回文件,失败的结果都是panic
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = match File::open("hello.txt") {
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),
},
other_error => panic!("Problem opening the file: {:?}", other_error),
}
};
}
我们发现正确时,我们Ok的操作是固定的,都是返回正确操作的结果,而错误处理的部分可以千奇百怪,往往需要各种match
- unwarp_of_else操作,等价于上份代码
fn main() {
let f = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Problem creating the file: {:?}", error);
})
} else {
panic!("Problem opening the file: {:?}", error);
}
});
}
失败时panic的简写
match太冗长了,Result定义了很多辅助方法来处理各种情况,比如unwrap unwrap
- unwarp_of_else操作,等价于上份代码
如果Result值是成员Ok,则unwrap返回Ok的值
- 如果Result值是成员Err,unwrap会帮我们调用panic
(如果一个函数里好多个unwrap,那就不方便知道具体是哪个unwrap报错
expect
类似于unwrap,但expect还额外支持自定义错误信息,方便排错 ```rust use std::fs::File;
fn main() {
//thread ‘main’ panicked at ‘called Result::unwrap()
on an Err
value: Error…
let f = File::open(“hello.txt”).unwrap();
//thread ‘main’ panicked at ‘Failed to open hello.txt: Error …
let f = File::open(“hello.txt”).expect(“Failed to open hello.txt”);
}
<a name="H3d4k"></a>
## 错误传播的简写
感觉有点类似于java的throw,当前函数不处理错误,传播到调用链上游去处理
<a name="tKkA3"></a>
#### 在返回Result的调用之后加一个 '?'
- 比如第二行,原本是返回Result类型,加了问号后就成了File类型
- 如果错误,则Err的值将作为整个函数的返回值直接返回,错误传播给了调用者
- 如果Ok,程序则继续执行
- 消除大量样板代码,也便于支持链式调用
```rust
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)
}
fn read_username_from_file() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
From trait
- 用来将错误从一种类型转换为另一种类型
- ? 运算符调用from函数时,运算符收到的错误类型,被转换为由当前函数返回类型所定义的错误类型
只要每个错误类型都实现了from函数,定义如何将自身转成返回的错误类型,就可以用 ?运算符 来自动处理
其他
match的return Err(e)要求返回值类型是Result,所以函数的返回值必须是Result才能和这个return兼容
- main函数中使用 ? 运算符是会报错的
- main返回值类型是 ()
- 只能在返回Result或者其他实现了std::ops::Try类型的函数中使用 ?运算符
- 如果一个函数不返回Result,并且我们在该函数的实现中调用返回Result的函数时还想使用?运算符,该怎么做呢?
- 将函数返回值改成Result
- 处理Result,使用match或者Result提供的其他方法,比如unwrap
- 将函数返回值改成Result
3、panic! or not
示例、代码原型和测试,适合panic
- 示例一些概念时,错误处理太多可能就忽视了概念本身,可以unwrap、expect或者panic
-
当我们很自信对于具体的问题非常清楚了,编译器小题大做了
我们知道Result一定会是Ok,于是unwrap就挺何时
比如我们像解析ip地址,而这个地址不是用户输入的,而是我们硬编码的正确地址,那么parse()的时候没必要认认真真地进行异常处理了错误处理指导原则
当可能导致有害状态时使用panic!
有害状态指某种保证、假设、协议等不可变因素被打破的状态,如无效值等
- 有害状态不包含预期会偶尔发生的错误
- 在此之后代码的运行不依赖于有害状态
当错误可以预期时Result更合适
- 比如HTTP请求返回的状态,是出发了限流,而不是其他严重问题的状态
可以创建自定义类型,并将错误处理的流程放到相关接口里,方便使用