简介
欢迎来到「惊慌与否」实验。本实验是 《Rust 程序设计语言》 的一部分。你可以在 LabEx 中练习 Rust 技能。
在本实验中,决定是调用 panic!
还是返回 Result
取决于错误情况的可恢复性以及调用代码可用的选项。
This tutorial is from open-source community. Access the source code
💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版
欢迎来到「惊慌与否」实验。本实验是 《Rust 程序设计语言》 的一部分。你可以在 LabEx 中练习 Rust 技能。
在本实验中,决定是调用 panic!
还是返回 Result
取决于错误情况的可恢复性以及调用代码可用的选项。
那么,你如何决定何时应该调用 panic!
,何时应该返回 Result
呢?当代码发生恐慌时,就无法恢复了。对于任何错误情况,无论是否有可能恢复,你都可以调用 panic!
,但这样你就是在替调用代码决定某个情况是无法恢复的。当你选择返回一个 Result
值时,你给了调用代码一些选择。调用代码可以选择以适合其情况的方式尝试恢复,或者它可以决定在这种情况下 Err
值是无法恢复的,所以它可以调用 panic!
,把你这个可恢复的错误变成一个不可恢复的错误。因此,当你定义一个可能失败的函数时,返回 Result
是一个很好的默认选择。
在示例、原型代码和测试等情况下,编写会发生恐慌的代码而不是返回 Result
更合适。让我们来探讨一下原因,然后讨论一些编译器无法判断失败不可能发生,但作为人类你可以判断的情况。本章将以一些关于如何决定在库代码中是否要发生恐慌的一般准则作为结尾。
当你编写一个示例来说明某个概念时,包含健壮的错误处理代码可能会使示例变得不那么清晰。在示例中,可以理解的是,对可能会导致恐慌的 unwrap
之类的方法的调用,是你希望应用程序处理错误的方式的占位符,具体处理方式可能会因代码的其他部分的功能而异。
同样,在你准备好决定如何处理错误之前进行原型设计时,unwrap
和 expect
方法非常方便。当你准备好让程序更健壮时,它们会在你的代码中留下清晰的标记。
如果在测试中方法调用失败,即使该方法不是正在测试的功能,你也希望整个测试失败。因为 panic!
是将测试标记为失败的方式,调用 unwrap
或 expect
正是应该发生的情况。
当你有其他逻辑确保 Result
将具有 Ok
值,但编译器无法理解该逻辑时,调用 unwrap
或 expect
也是合适的。你仍然需要处理一个 Result
值:无论你调用的是什么操作,通常仍然有可能失败,即使在你的特定情况下从逻辑上讲是不可能的。如果你可以通过手动检查代码确保永远不会有 Err
变体,那么调用 unwrap
是完全可以接受的,甚至更好的做法是在 expect
的文本中记录你认为永远不会有 Err
变体的原因。下面是一个例子:
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
改为更好的错误处理代码。
当你的代码可能最终处于不良状态时,最好让代码发生恐慌。在这种情况下,“不良状态”是指某些假设、保证、约定或不变量被打破,例如当无效值、矛盾值或缺失值被传递给你的代码时,再加上以下一种或多种情况:
如果有人调用你的代码并传入不合理的值,如果你可以的话,最好返回一个错误,以便库的用户可以决定在这种情况下他们想做什么。然而,在继续可能不安全或有害的情况下,最好的选择可能是调用 panic!
,并提醒使用你的库的人注意他们代码中的错误,以便他们可以在开发过程中修复它。同样,如果你调用无法控制的外部代码,而它返回一个你无法修复的无效状态,panic!
通常也是合适的。
然而,当预期会失败时,返回一个 Result
比调用 panic!
更合适。示例包括解析器接收到格式错误的数据,或者 HTTP 请求返回一个表示你已达到速率限制的状态。在这些情况下,返回一个 Result
表示失败是一种预期的可能性,调用代码必须决定如何处理。
当你的代码执行一个操作,如果使用无效值调用可能会使用户面临风险时,你的代码应该首先验证值是否有效,如果值无效则发生恐慌。这主要是出于安全原因:尝试对无效数据进行操作可能会使你的代码暴露于漏洞。这就是为什么如果你尝试进行越界内存访问,标准库会调用 panic!
的主要原因:试图访问不属于当前数据结构的内存是一个常见的安全问题。函数通常有“契约”:只有当输入满足特定要求时,它们的行为才得到保证。当契约被违反时发生恐慌是有意义的,因为契约违反总是表明调用方存在错误,并且这不是一种你希望调用代码必须显式处理的错误类型。实际上,调用代码没有合理的方法来恢复;调用的程序员需要修复代码。函数的契约,特别是当违反契约将导致恐慌时,应该在函数的 API 文档中进行解释。
然而,在所有函数中进行大量的错误检查会很冗长且烦人。幸运的是,你可以使用 Rust 的类型系统(以及因此由编译器完成的类型检查)为你进行许多检查。如果你的函数有一个特定类型作为参数,你可以在知道编译器已经确保你有一个有效值的情况下继续你的代码逻辑。例如,如果你有一个类型而不是 Option
,你的程序期望有“某个东西”而不是“没有东西”。然后你的代码不必处理 Some
和 None
变体的两种情况:它只会有一种肯定有值的情况。试图将没有值传递给你的函数的代码甚至不会编译,所以你的函数不必在运行时检查那种情况。另一个例子是使用无符号整数类型,如 u32
,它确保参数永远不会为负。
让我们进一步运用 Rust 的类型系统来确保我们拥有一个有效值的理念,看看如何创建一个用于验证的自定义类型。回忆一下第 2 章中的猜数字游戏,在那个游戏中,我们的代码要求用户猜一个 1 到 100 之间的数字。在将用户的猜测与我们的秘密数字进行比较之前,我们从未验证过用户的猜测是否在这个范围内;我们只验证了猜测是否为正数。在这种情况下,后果并不是很严重:我们输出的“太高”或“太低”仍然是正确的。但是,如果能引导用户进行有效的猜测,并在用户猜测超出范围与用户输入字母等情况时表现出不同的行为,那将是一个很有用的改进。
一种实现方法是将猜测解析为 i32
而不仅仅是 u32
,以允许可能的负数,然后添加一个检查数字是否在范围内的操作,如下所示:
文件名:src/main.rs
loop {
--snip--
let guess: i32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
if guess < 1 || guess > 100 {
println!("秘密数字将在 1 到 100 之间。");
continue;
}
match guess.cmp(&secret_number) {
--snip--
}
if
表达式检查我们的值是否超出范围,告诉用户问题所在,并调用 continue
开始循环的下一次迭代并要求再次猜测。在 if
表达式之后,我们可以继续进行 guess
和秘密数字之间的比较,因为我们知道 guess
在 1 到 100 之间。
然而,这并不是一个理想的解决方案:如果程序绝对必须只对 1 到 100 之间的值进行操作,并且有许多函数都有这个要求,那么在每个函数中都进行这样的检查会很繁琐(并且可能会影响性能)。
相反,我们可以创建一个新类型,并将验证放在一个函数中,以创建该类型的实例,而不是在各处重复验证。这样,函数在其签名中使用新类型并自信地使用它们接收到的值就是安全的。清单 9-13 展示了一种定义 Guess
类型的方法,只有当 new
函数接收到一个 1 到 100 之间的值时,才会创建 Guess
的实例。
文件名:src/lib.rs
1 pub struct Guess {
value: i32,
}
impl Guess {
2 pub fn new(value: i32) -> Guess {
3 if value < 1 || value > 100 {
4 panic!(
"猜测值必须在 1 到 100 之间,得到的是 {}.",
value
);
}
5 Guess { value }
}
6 pub fn value(&self) -> i32 {
self.value
}
}
清单 9-13:一个只接受 1 到 100 之间值的 Guess
类型
首先,我们定义一个名为 Guess
的结构体,它有一个名为 value
的字段,用于存储一个 i32
[1]。这就是数字将被存储的地方。
然后,我们在 Guess
上实现一个关联函数 new
,用于创建 Guess
值的实例 [2]。new
函数被定义为有一个名为 value
的 i32
类型参数,并返回一个 Guess
。new
函数体中的代码会测试 value
,以确保它在 1 到 100 之间 [3]。如果 value
没有通过这个测试,我们就进行一次 panic!
调用 [4],这将提醒编写调用代码的程序员他们有一个需要修复的错误,因为创建一个 value
超出此范围的 Guess
将违反 Guess::new
所依赖的契约。Guess::new
可能会发生恐慌的情况应该在其面向公众的 API 文档中进行讨论;我们将在第 14 章中介绍在你创建的 API 文档中表明可能发生 panic!
的文档约定。如果 value
通过了测试,我们就创建一个新的 Guess
,将其 value
字段设置为 value
参数,并返回这个 Guess
[5]。
接下来,我们实现一个名为 value
的方法,它借用 self
,没有任何其他参数,并返回一个 i32
[6]。这种方法有时被称为“获取器”,因为它的目的是从其字段中获取一些数据并返回它。这个公共方法是必要的,因为 Guess
结构体的 value
字段是私有的。value
字段为私有很重要,这样使用 Guess
结构体的代码就不允许直接设置 value
:模块外部的代码必须使用 Guess::new
函数来创建 Guess
的实例,从而确保不可能有一个 Guess
的 value
没有经过 Guess::new
函数中的条件检查。
然后,一个函数如果其参数或返回值只在 1 到 100 之间的数字,那么它可以在其签名中声明它接受或返回一个 Guess
而不是 i32
,并且在其函数体中不需要进行任何额外的检查。
恭喜你!你已经完成了“是否恐慌”实验。你可以在 LabEx 中练习更多实验来提升你的技能。