32_何时使用panic

496 阅读4分钟

1. 总体原则

在rust里错误主要分为两类,一类是可恢复的,另一类是不可恢复的,通过调用panic!宏代表该错误不可恢复。通过返回Result则相当于错误进行了传播,而这类错误是可恢复的。

当我们自己写的代码,在某些情况下决定为不可恢复错误的时候,我们可以使用panic!。如果我们写的函数,如果某些情况决定为错误可以恢复,可以返回Resul,这时候将错误的处理权交给了代码的调用者,代码的调用者要根据实际情况决定是否恢复此错误。总结来说如下:

  • 在定义一个可能失败的函数时,优先考虑返回Result
  • 如果某种情况出现的失败失败肯定不可恢复的,就使用panic!

2. 使用panic!的场景如下

  • 演示某些概念:使用unwrap函数,发生错误将使用panic!标记;
  • 原型代码:unwrap、expect函数,在原型代码里还不知道如何错误错误,使用unwrap、expect这类方法将使用panic!标记错误,发生错误时便于跟踪问题;
  • 测试:使用unwrap、expect函数,测试的代码如果出现错误,会被panic!立即停止。

3. 错误处理的指导性建议

当代码最终可能处于损坏状态时,最好使用panic!

损坏状态(bad state)表示某些假设、保证、约定或不可变性被打破,如下列情况都可以被认定为损坏状态:

  • 非法的值、矛盾的值、或者空缺的值被传入了代码
  • 这种状态并不能预测,而是偶尔发生
  • 如果你的代码处于这种状态就无法运行
  • 在你使用的数据类型中没有一个更好的方法来将这些(处于损坏状态)的信息进行编码

如果不理解以上的概述,我们可以参考以下几个建议使用panic!或者返回Resul的场景

  • 当你的代码被调用时,传入了无意义的参数值:panic!
  • 调用外部不可控代码,返回非法状态,你无法修复:panic!
  • 如果失败是可以预期的:Result,比如我们想把一个字符串解析成数字,这时候可能成功,可能失败,是可预期的
  • 当你的代码对某些值进行操作,首先验证这些值的合法性,如果不合法,进行panic!。如果输入数据不合法,往往是因为调用者逻辑出现了问题,也不应该由调用者进行解决,应该就地进行panic!

4. 数据验证逻辑的优化

针对调用者可能传入非法值的情况,我们可以进行逻辑优化,在创建我们需要的数据类型时,我们在构造函数里实现验证逻辑,我们则无需担心接收的值的有效性。如在该系列文章的猜数游戏中,可以有以下逻辑,验证逻辑

// ... 前面的逻辑忽略掉
// 先将guess变量转换为整数类型
let guess: i32 = match guess.trim().parse() {
    // 解析正确时将数字返回
    Ok(num) => num,
    // 解析错误时,使用 continue 跳出本次循环,进行下一次循环
    Err(_) => continue,
};

// 验证传入数值必须大于1、小于100
if guess < 1 || guess > 100 {
    println!("The secret number will be between 1 and 100");
    continue;
}

// ... 后面的逻辑忽略掉

我们改为以下逻辑

pub struct Guess {
    value: i32,
}

impl Guess {
    // 定义一个关联函数,相当于构造函数
    pub fn new(value: i32) -> Guess {
        if value < 1 || value > 100 {
            panic!("The secret number will be between 1 and 100");
        }

        Guess { value }
    }

    // 定义获取值的方法
    pub fn value(&self) -> i32 {
        self.value
    }
}

fn main() {
    loop {
        // ...省略前面的逻辑

        // 先将guess变量转换为整数类型
        let guess: u32 = match guess.trim().parse() {
            // 解析正确时将数字返回
            Ok(num) => num,
            // 解析错误时,使用 continue 跳出本次循环,进行下一次循环
            Err(_) => continue,
        };

        let guess = Guess::new(guess);

        // ...省略后面的逻辑
    }
}

在以上代码中,我们将解析的数据传入Guess::new,如果不发生panic!,代码数据合法,通过了验证;如果发生了panic!,说明传入数据没有通过验证。使用此方式进行数据,优化了验证的过程,无需把验证的逻辑到处写,提高了代码的可读性。

要注意的是:在上面的Guess结构体中,必须把value属性设置为私有,再通过结构体方法获取属性的数据。我们不允许通过Guess结构体直接修改value的值,在模块外的代码必须使用Guess::new函数来创建Guess的实例。这样能确保value值通过了有效性的检查,通过检查则产生实例,否则则发生panic!