Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

9.4 什么时候该使用panic!

9.4.1 总体原则

第9.1章「不可恢复的错误以及panic!中也讲了,Rust中的错误类型有两种:可恢复的和不可恢复的。

调用panic!就相当于发生了一个不可恢复的错误。返回Result类型意味着错误得到了传播,而且这类错误是可恢复的。

当你认为自己可以代替调用你代码的调用者,来决定某些情况是不可恢复的时候,就可以写panic!

如果你写的函数返回的是Result,就相当于你把错误的处理权交给了代码的调用者。调用者就可以根据实际情况来决定是否要恢复这个错误,当然它也可以觉得这个错误是不可恢复的,然后自己调用panic!

总而言之,如果你定义的是一个可能失败的函数,那么优先考虑返回Result;如果你认为某种情形是肯定不可恢复的,那就使用panic!

9.4.2 panic!适用的场景

编写示例代码、用于演示某些概念的时候,可以使用panic!。在这类程序里面,处理错误通常是使用unwrap这类可以制造恐慌的办法。在这里,unwrap就相当于一个占位符,然后针对不同功能的不同错误再分别编写代码进行个性化的处理。

在编写原型代码时可以使用panic!。因为在编写这类代码时还没想好该怎么处理错误,unwrapexpect方法在原型设计时非常方便,因为它们能制造恐慌,在代码中留下清晰的标记,后续就可以根据这些记号来对这些错误进行更具体的处理。

在编写测试代码时可以使用panic!。因为如果测试代码中的某个方法调用失败了,那么整个测试就应该被认定为失败,而失败状态正可以通过panic!来标记。

9.4.3 你比编译器更了解情况

有时候你可以确定一个函数调用返回的一定是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 为验证创建自定义类型

2.1. 猜数游戏 Pt.1为例,有些代码不重要就跳过了:

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这个方法来获取。