喜欢的话别忘了点赞、收藏加关注哦,对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
9.4.1. 总体原则
在9.1. 不可恢复的错误以及panic!中也讲了Rust中的错误类型有两种:可恢复的和不可恢复的。
调用panic!
就相当于发生了一个不可恢复的错误。返回Result类型,这类错误就得到了传播,而且这类错误是可恢复的。
当你认为自己可以替代调用你代码的调用者来决定某些情况是不可恢复的时候,就可以写panic!
。
如果你写的函数返回的是Result,就相当于你把错误的处理权交给了代码的调用者,调用者就可以根据实际情况来决定是否要恢复这个错误,当然它也可以觉得这个错误是不可恢复的然后调用panic!
来进行恐慌。
总而言之,如果你定义的是一个可能失败的函数,那么优先考虑返回Result类型,如果你认为某种情形是肯定不可恢复的,那就使用panic!
。
9.4.2.panic!
适用的场景
编写示例,用于演示某些概念的时候可以使用panic!
。在这类程序里面处理错误通常是使用unwrap
这类可以制造恐慌的办法。在这里unwrap
就相当于一个占位符,然后针对不同功能的不同错误再分别的编写代码进行个性化的处理。
在编写原型代码时可以使用panic!
。因为在编写这类代码时还没想好该怎么处理错误, unwrap
和expect
方法在原型设计时非常方便,因为它们能制造恐慌,在代码中留下清晰的标记,后续就可以根据记号来对这些错误进行进一步的处理。
在编写测试代码时可以使用panic!
。因为如果测试代码中的某个方法调用失败了,那么整个测试就应该被认定为失败,而失败状态正可以通过panic!
来标记。
9.4.3. 你比编译器更了解情况
有时候你可以确定一个函数的调用返回的Result一定是Ok
的,绝对不会出现恐慌,这个时候就可以使用unwrap
。但是由于返回类似是Result,所以编译器仍然认为它可能出错,但你知道它一定不可能。
看个例子:
use std::net::IpAddr;
fn main(){
let home: IpAddr = "127.0.0.1".parse().unwrap();
}
这个例子使用了IpAddr
这个枚举,在main
函数中写了"127.0.0.1",对它进行解析,我们知道"127.0.0.1"是一个合理的IP地址,返回值一定是Ok
,所以后面就可以使用unwrap
,它绝对不会出现恐慌。
9.4.5. 错误处理的指导性建议
当你的代码最终可能处于损坏状态(Bad State)时,最好使用panic!
。损坏状态是指某些假设、保证、约定或不可变性被打破了。
比如说一些非法的值、矛盾的值或是空缺的值被传入代码。以及下列中的任意一条:
- 这种损坏状态是一个意外
- 在此之后的代码如果处于这种损坏状态就无法运行
- 使用的类型中没有一个好方法来将这些处于损坏状态的信息进行编码
还是看一下具体的场景:
- 传入了无意义的参数值:
panic!
- 调用外部不可控代码,返回非法状态,你又无法进行修复:
panic!
- 如果失败是可预期的(比如把字符串解析为数字):Result
- 当你的代码对值进行操作,首先应该验证值的合法性,如果不合法:
panic!
这一点主要出于安全性的考虑,因为在尝试基于某些非法的值去进行操作的时候,就可能会暴露代码中的漏洞。这也是标准库会在代码尝试越界访问时报错的原因,因为尝试访问不属于当前数据结构的内存是一个普遍的安全问题。
而且函数通常是有某种约定的,就是只有在输入数据满足某些特定条件下才能够正常运行,而在约定被违反时就应该出发恐慌。因为破坏这些约定往往预示着调用者端产生了bug,而因此产生的错误也不应该由调用者来进行解决,应该就地正法,出发恐慌。
9.4.6. 为验证创建自定义类型
以第二章讲的猜数游戏为例,有些代码不重要就跳过了:
fn main() {
loop {
// --snip--
let guess: i32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
if guess < 1 || guess > 100 {
println!("The secret number will be between 1 and 100.");
continue;
}
match guess.cmp(&secret_number) {
// --snip--
}
}
这里对原本的代码进行了一些修改:
- 把
guess
的值从u32
改为i32
,这样就能接收负数 - 对于用户的输入是小于1大于100的情况,提醒用户神秘数字在1到100中间
如果字符串转整形解析失败,那么就会触发continue
进行下一次迭代;如果数字的范围不在1到100之间,还会触发continue
进行下一次迭代。针对这个小程序,可以把验证直接写在main
函数里面,如果是一个大项目,每个函数都需要验证的话,那在每个函数里都写一遍验证逻辑就是相当麻烦的了。
针对这种情况,可以创建一个新的类型,把验证逻辑放到构造这个新类型实例的函数里就行,这样子只有通过验证的才能成功创建出实例,后续不需要担心所接收值的有效性。
看下例子:
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {value}.");
}
Guess { value }
}
pub fn value(&self) -> i32 {
self.value
}
}
fn main() {
loop {
// --snip--
let guess: i32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
let guess = Guess::new(guess);
match guess.value().cmp(&secret_number) {
// --snip--
}
}
new
就是实例构造器,如果值不在1到100间就会panic!
,如果没发生恐慌的话,那就创建一个Guess
实例,value
的值就是传入的值。
还定义了一个方法叫value
,它会提取这个struct
里value
字段的值返回。
下面的main
函数里就可以删掉验证值是否在1到100间的操作了,转而使用Guess::new
这个构造器来验证。
如果要使用到guess
的实际值,比如说match
的时候,就可以使用value
这个方法来获取。