panic! 还是不要 panic!

To panic! or Not to panic!

那么,当代码死机无法恢复时,咱们要如何决定何时应该调用 panic!,何时应返回 Result 呢?对于任何错误情形,无论是否有可能恢复,咱们都可以调用 panic!,但这样一来,咱们就代表调用代码,做出了无法恢复的决定。在咱们选择返回一个 Result 值时,咱们就给了调用代码一些选择。调用代码可以选择以适合其情况的方式尝试恢复,或者他可以决定在这种情况下的某个 Err 值是不可恢复的,因此他可以调用 panic! 并将咱们的可恢复错误,变成一个不可恢复的错误。因此,在咱们定义某个可能失败的函数时,返回 Result 是一个不错的默认选择。

在示例程序、原型代码及测试等情况下,编写会死机而非返回一个 Result 的代码更为合适。我们来探讨一下原因,然后讨论在哪些情况下,编译器无法区分出失败是不可行的,但作为人类咱们却可以。本章最后将介绍一些,关于如何决定是否要在库代码中使用执行死机的一般指导原则。Let's explore why, then discuss situations in which the compiler can't tell that failure is impossible, but you as a human can. The chapter will conclude with some general guidelines on how to decide whether to panic in library code.

示例程序、原型代码与测试

Examples, Prototype Code, and Tests

在咱们编写说明某些概念的示例程序时,同时包含一些健壮的错误处理代码,会使示例程序变得不那么明了。在示例程序中,到像 unwrap 这样可能会死机的某个方法的调用,往往是作为咱们打算咱们应用程序处理错误方式的占位符,而根据咱们代码其余部分的操作,咱们程序处理错误方式可能不一样。

同样,在咱们原型设计时,准备好决定如何处理错误前,unwrapexpect 两个方法非常方便。他们会在咱们的代码中,留下一些清晰的标记,以便咱们准备好让程序更健壮时使用。

如果一次测试中的某个方法调用失败,咱们会希望整个测试都失败,即使该方法并不属于被测试的功能。因为 panic! 是标记测试失败的方式,调用 unwrapexpect 正是不二之选。

相比于编译器,咱们掌握了更多信息的情况

Cases in Which You Have More Information Than the Compiler

当咱们有其他确保 Result 值将有着一个 OK 值的逻辑时,调用 unwrapexpect 也是合适的,但编译器并不理解这些逻辑。咱们仍将有个咱们需要处理的 Result 值:无论咱们调用的是何种操作,在一般情况下都有可能失败,即使在咱们特定情况下该操作逻辑上是不可行的。若咱们能通过手动检查代码,确保咱们绝不会有个 Err 变种,那么调用 unwrap 就是完全可接受的,甚至最好在 expect 文本中,记录下咱们认为不会出现 Err 变种的原因。下面是个例子:

fn main () {
    use std::net::IpAddr;

    let home: IpAddr = "127.0.0.1"
        .parse()
        .expect("硬编码的 IP 地址应是有效的");
}

通过解析一个硬编码字符串,我们创建出一个 IpAddr 实例。我们可以看到,127.0.0.1 是个有效的 IP 地址,因此在这里使用 expect 是可接受的。然而,有着一个硬编码的有效字符串,并不会改变 parse 这个方法的返回类型:我们仍会得到一个 Result 值,同时编译器仍将让我们按照 Err 变种为一种可能性一样,处理这个 Result,因为编译器还不够聪明,无法发现这个字符串始终是个有效的 IP 地址。在这个 IP 地址字符串是来自用户,而不是硬编码到程序中的时,就因此 确实 存在失败的可能性,那么我们肯定就会希望以一种更健壮的方式,处理这个 Result。在将来我们需要从其他来源获取该 IP 地址时,那么提及这个 IP 地址是硬编码的假设,将提醒我们将 expect 修改成更好的错误处理代码。

错误处理指南

Guidelines for Error Handling

当咱们的代码有可能陷入某种糟糕的状态时,那么最好让咱们的代码死机。在此语境下,所谓 糟糕状态,bad state 是指某个假定、保证、合约,或恒定值被破坏,例如在无效值、矛盾值或缺失值被传递给咱们的代码时 -- 再加上以下一种或多种情况:

  • 糟糕状态是指意想不到的某种情况,与偶尔会发生的情况相反,比如错误格式的用户输入数据等;

  • 咱们此处之后的代码,需要依赖于不再处于这种不良状态,而不是在每一步都检查问题;

  • 并无以咱们所使用类型,编码这些信息的某种良好方式。我们将在第 18 章 “将状态和行为编码为类型” 中,举例说明这个意思。

如果有人调用了咱们的代码并传入不合理的值,那么最好是咱们尽可能返回某个错误,这样该库的用户就可以决定,他们在这种情况下要做什么。但是,如果继续下去可能不安全或有害,那么最好的选择就可能是调用 panic!,并提醒使用咱们库的人他们代码中的错误,以便他们在开发过程中修复错误。类似地,若咱们调用的外部代码不在咱们的掌控中,而他返回了咱们无法修复的某种无效状态,这时通常也适合调用 panic!

不过,当失败属于预期中的时,返回一个 Result 就比调用一次 panic! 更合适。例如,某个解析器得到畸形数据,或者某次 HTTP 请求返回了一个表明咱们已达速率限制的状态时。在这些情况下,返回一个 Result 就表明失败是预期的、调用代码必须决定如何处理的一种可能。

当咱们的代码执行某项若其调用的是无效值时,可能会给用户带来风险的操作时,咱们的代码就应首先检查这些值是有效的,并在值无效时死机。这主要是出于安全考虑:尝试在无效数据上操作,会使咱们的代码暴露于漏洞之中。这也是标准库会在咱们尝试越界内存访问时,调用 panic! 的主要原因:尝试访问不属于当前数据结构的内存,是个常见的安全问题。函数通常都有 合约,contracts:只有在输入满足特定要求时,他们的行为才有保证。在这种合约破坏时死机是有道理的,因为合约破坏总是表明某个调用方的错误,而这并不是咱们希望调用代码必须要显式处理的错误类型。事实上,并没有调用代码恢复的合理方式;调用的 程序员 需要修复代码。函数的合约,尤其是在破坏会导致死机时,应在该函数的 API 文档中加以说明。

然而,在所有咱们的函数中,进行大量的错误检查会显得冗长而烦人。幸运的是,咱们可使用 Rust 的类型系统(及因此由编译器完成的类型检查),为咱们完成许多的检查。若咱们的函数有某个特定类型的参数,咱们就可以在知悉编译器已确保了咱们有个有效值下,处理咱们代码的逻辑。例如,在咱们有个类型而不是 Option 时,那么咱们的程序就会期望得到 某个东西,something,而不是 全无,nothing。这样,咱们的代码就不必处理 SomeNone 变种的两种情况:他只有肯定有某个值的一种情况。试图向咱们的函数传递全无的代码,甚至不会编译,因此咱们函数在运行时就不必检查这种情况。另一个例子是使用如 u32 的无符号整数类型,这确保参数绝不会是负数。

创建用于验证的定制类型

Creating Custom Types for Validation

我们来把使用 Rust 的类型系统,确保我们有个有效值的想法向前推进一步,看看如何创建出一个用于验证的定制类型。请回顾第 2 章中的猜数游戏,其中我们的代码要求用户猜一个 1100 之间的数字。在将用户的猜数与我们的秘密数字核对前,我们从未验证过其是否介于这两个数字之间;我们只验证了用户猜数是个正数。在这种情况下,后果并不严重:我们“太大”或“太小”的输出,将仍然是正确的。但是,往有效猜数引导用户,并在用户猜的数字超出范围时,及用户输入字母等时有不同的行为,这将是个非常有用的增强功能。

实现这一增强的一种方法,是将猜测值解析为 i32,而不是 u32,允许出现潜在的负数,然后添加一个数字在范围内的检查,就像这样:

文件名:src/main.rs

#![allow(unused)]
fn main() {
        loop {
            // --跳过--
            let guess: i32 = match guess.trim().parse()
            {
                Ok(num) => num,
                Err(_) => {
                    println! ("请输入一个数字!");
                    continue
                },
            };

            if guess < 1 || guess > 100 {
                println!("秘密数字将在 1 和 100 之间。");
                continue;
            }

            match guess.cmp(&secret_number) {
                // --跳过--
        }
}

其中 if 表达式会检查我们的值是否超出范围,告诉用户问题所在,并调用 continue 开始循环的下一次迭代并请求另一个猜数。在这个 if 表达式后,我们可以在清楚 guess 介于 1100 之间下,继续 guess 与秘密数字间的比较。

然而,这不是一种理想解决方案:在程序只对 1100 间的值进行操作绝对重要,并且程序有着许多有此要求的函数,那么在每个函数中都进行这样的检查,将是非常乏味的(而且可能会影响性能)。

相反,我们可在一个专门模组中构造一个新类型,并把这些验证放入一个函数中,从而创建出该类型的实例,而不是在各处重复这些验证。这样,函数就可以安全地在其签名中使用这个新类型,并放心地使用接收到的值。下面清单 9-13 展示了定义 Guess 类型的一种方法,只有在 new 函数接收到介于 1100 之间的值时,才会创建出一个 Guess 的实例。

文件名:src/guessing_game.rs

#![allow(unused)]
fn main() {
pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 || value > 100 {
            panic!("猜数值必须在 1 与 100 之间,得到了 {value}。");
        }

        Guess { value }
    }

    pub fn value(&self) -> i32 {
        self.value
    }
}
}

清单 9-13:只有在处于 1100 之间的值下才会继续的 Guess 类型

首先,我们创建了个名为 guessing_game 的新模组。接着,我们在该模块中定义了个名为 Guess 的结构体,该结构体有个名为 value 的字段,其中存放着一个 i32。数字将存储于该处。

然后,我们在 Guess 上实现了一个名为 new 的关联函数,创建 Guess 值的实例。new 函数被定义为有个名为 value,类型为 i32 的参数,并返回一个 Guessnew 函数主体中的代码,会测试 value 确保他介于 1100 之间。在 value 没有通过此测试时,我们进行一次 panic! 调用,这将提醒编写调用代码的程序员,他们有一个需要修复的错误,因为以在此范围之外的一个 value 创建一个 Guess,将违反 Guess::new 所依赖的合约。Guess::new 可能会死机的那些条件,应该在面向公众的 API 文档中被提及;我们将在第 14 章中,介绍在咱们创建的 API 文档中,说明 panic! 可能性的文档约定。在 value 确实通过了测试时,我们就会创建出一个新的 Guess,并将其 value 字段设置为参数 value,然后返回该 Guess

接下来,我们实现了一个名为 value 的方法,该方法借用了 self,不带任何其他参数,并返回一个 i32。这种类别的方法,有时被称为 getter,因为他的目的是获取其字段中的某个数据并返回。这个公共方法是必要的,因为 Guess 结构体的 value 字段是私有的。value 这个字段是私有的很重要,这样使用 Guess 这个结构体的代码,就不能直接设置 valueguessing_game 模组之外的代码,必须 使用 Guess::new 函数创建 Guess 的实例,从而确保某个 Guess 不可能有着一个,未经 Guess::new 函数中条件检查过的 value

某个有着参数或返回值仅为 1100 之间数字的函数,随后就可以在其签名中,声明他接收或返回的是 Guess 而不是 i32,并且不需要在其主体中进行任何额外的检查。

本章小结

Rust 的这些错误处理特性,旨在帮助咱们编写更健壮的代码。

  • panic! 这个宏表示咱们的程序处于其无法处理的某种状态,让咱们可以告诉进程停止,而不是尝试继续处理无效或不正确的值;
  • Result 枚举使用 Rust 的类型系统,表示可能会失败,但咱们代码可从中恢复的那些操作。咱们可以使用 Result 告诉调用咱们代码的代码,他也需要处理潜在的成功或失败。

在恰当情形下合理使用 panic!Result,可以让咱们的代码在面对不可避免的问题时更加可靠。

现在,咱们已经看到标准库以 OptionResult 两个枚举,使用泛型的一些有用方法,下面我们将讨论泛型的工作原理,以及如何在咱们的代码中使用泛型。

(End)

Last change: 2025-07-02, commit: 123c071

小额打赏,赞助 xfoss.com 长存......

微信 | 支付宝

若这里内容有帮助到你,请选择上述方式向 xfoss.com 捐赠。