是否使用panic!
那么,你如何决定何时应该调用 panic!
以及何时应该返回 Result
?当代码 panic 时,没有恢复的方法。你可以在任何错误情况下调用 panic!
,无论是否有恢复的可能,但这样做你是在代表调用代码决定某个情况是不可恢复的。当你选择返回一个 Result
值时,你给调用代码提供了选项。调用代码可以选择以适合其情况的方式尝试恢复,或者它可以决定在这种情况下 Err
值是不可恢复的,因此它可以调用 panic!
并将你的可恢复错误转换为不可恢复的错误。因此,当你定义一个可能失败的函数时,返回 Result
是一个很好的默认选择。
在示例、原型代码和测试等情况下,编写会引发恐慌的代码比返回一个Result
更为合适。让我们探讨其中的原因,然后讨论编译器无法判断失败是不可能的情况,但作为人类的你可以判断。本章将以一些通用指南作为结束,这些指南将帮助你决定在库代码中是否应该引发恐慌。
示例、原型代码和测试
当你编写示例来说明某个概念时,包含健壮的错误处理代码可能会使示例变得不那么清晰。在示例中,理解为对像 unwrap
这样的方法的调用,可能会引发 panic,是作为你希望应用程序处理错误的方式的占位符,这可以根据你的其余代码的不同而有所不同。
同样,在原型设计时,unwrap
和 expect
方法非常方便,可以在你决定如何处理错误之前使用。它们在你的代码中留下了清晰的标记,以便你在准备使程序更加健壮时使用。
如果在测试中方法调用失败,即使该方法不是测试的功能,你也希望整个测试失败。因为 panic!
是标记测试失败的方式,所以调用 unwrap
或 expect
正是应该发生的事情。
您比编译器拥有更多信息的情况
在你有其他逻辑确保 Result
将会有一个 Ok
值,但这些逻辑不是编译器能理解的情况下,调用 unwrap
或 expect
也是合适的。你仍然需要处理一个 Result
值:你调用的任何操作在一般情况下仍然有可能失败,即使在你的特定情况下逻辑上是不可能的。如果你可以通过手动检查代码确保永远不会有一个 Err
变体,调用 unwrap
是完全可以接受的,甚至更好是在 expect
文本中记录你认为永远不会有一个 Err
变体的原因。这里有一个例子:
fn main() { use std::net::IpAddr; let home: IpAddr = "127.0.0.1" .parse() .expect("Hardcoded IP address should be valid"); }
我们正在通过解析一个硬编码的字符串来创建一个 IpAddr
实例。我们可以看到 127.0.0.1
是一个有效的 IP 地址,因此在这里使用 expect
是可以接受的。然而,即使有一个硬编码的有效字符串,也不会改变 parse
方法的返回类型:我们仍然会得到一个 Result
值,编译器仍然会要求我们像 Err
变体有可能出现一样处理 Result
,因为编译器不够智能,无法识别这个字符串始终是一个有效的 IP 地址。如果 IP 地址字符串来自用户而不是硬编码在程序中,因此确实有可能失败,我们肯定希望以更健壮的方式处理 Result
。提到这个 IP 地址是硬编码的,会促使我们在将来需要从其他来源获取 IP 地址时,将 expect
更改为更好的错误处理代码。
错误处理指南
建议在代码可能进入不良状态时让代码引发恐慌。在此上下文中,不良状态是指某些假设、保证、契约或不变量被破坏的情况,例如当无效值、矛盾值或缺失值被传递给代码时—加上以下一项或多项:
- 坏状态是指那些意外发生的情况,而不是像用户偶尔输入错误格式数据那样可能会经常发生的情况。
- 您的代码从这一点之后需要依赖于不再处于这种不良状态,而不是在每一步都检查问题。
- 你使用的类型中没有一个好的方法来编码这些信息。我们将在第 18 章的 “将状态和行为编码为类型” 部分通过一个例子来说明我们的意思。
如果有人调用你的代码并传入了没有意义的值,最好能够返回一个错误,这样库的使用者可以决定在这种情况下他们想要做什么。然而,在继续执行可能会不安全或有害的情况下,最好的选择可能是调用panic!
并提醒使用你库的人他们的代码中的错误,以便他们在开发过程中修复它。同样,如果你调用的外部代码不受你控制,并且它返回了一个你无法修复的无效状态,那么调用panic!
通常是合适的。
然而,当失败是可以预期的时,返回一个 Result
比调用 panic!
更合适。例如,解析器接收到格式错误的数据或 HTTP 请求返回的状态表明你已达到速率限制。在这些情况下,返回一个 Result
表示失败是一个预期的可能性,调用代码必须决定如何处理。
当您的代码执行可能因使用无效值而使用户面临风险的操作时,您的代码应首先验证这些值是否有效,如果值无效则应触发 panic。这主要是出于安全考虑:尝试在无效数据上进行操作可能会使您的代码暴露于漏洞之中。这是标准库在您尝试越界内存访问时会调用 panic!
的主要原因:尝试访问不属于当前数据结构的内存是一个常见的安全问题。函数通常有契约:只有当输入满足特定要求时,其行为才能得到保证。当契约被违反时触发 panic 是合理的,因为契约违反总是表明调用方存在错误,而且这不是调用代码需要显式处理的那种错误。事实上,调用代码没有合理的方法可以恢复;调用的程序员需要修复代码。函数的契约,尤其是当违反契约会导致 panic 时,应在函数的 API 文档中进行解释。
然而,在所有函数中进行大量的错误检查会显得冗长且烦人。幸运的是,你可以利用 Rust 的类型系统(以及编译器进行的类型检查)来为你完成许多检查。如果函数的参数具有特定类型,你可以确信编译器已经确保你有一个有效的值,从而继续执行代码的逻辑。例如,如果你有一个类型而不是一个 Option
,你的程序期望有一个 东西 而不是一个 空值。你的代码就不必处理 Some
和 None
变体的两种情况:它将只有一个确定有值的情况。试图将空值传递给你的函数的代码甚至无法编译,因此你的函数在运行时不必检查这种情况。另一个例子是使用无符号整数类型,如 u32
,这确保参数永远不会是负数。
为验证创建自定义类型
让我们进一步利用 Rust 的类型系统来确保我们有一个有效的值,并看看如何为验证创建一个自定义类型。回想第 2 章中的猜数字游戏,我们的代码要求用户猜一个 1 到 100 之间的数字。我们从未验证用户的猜测是否在这些数字之间,而只是验证了猜测是否为正数。在这种情况下,后果并不严重:我们输出的“太高”或“太低”仍然是正确的。但是,引导用户进行有效的猜测,并在用户猜测的数字超出范围时与用户输入字母等其他情况时有不同的行为,这将是一个有用的增强。
一种方法是将猜测解析为 i32
而不仅仅是 u32
以允许可能的负数,然后添加一个检查数字是否在范围内的检查,如下所示:
if
表达式检查我们的值是否超出范围,告知用户问题,并调用 continue
开始循环的下一次迭代并要求再次猜测。在 if
表达式之后,我们可以进行 guess
和秘密数字之间的比较,确信 guess
在 1 和 100 之间。
然而,这并不是一个理想的解决方案:如果程序绝对需要仅对1到100之间的值进行操作,并且有许多函数都有这个要求,那么在每个函数中都进行这样的检查会很繁琐(并且可能影响性能)。
相反,我们可以创建一个新类型,并将验证放在一个函数中来创建该类型的实例,而不是在每个地方重复验证。这样,函数可以在其签名中安全地使用新类型,并且可以自信地使用接收到的值。清单 9-13 展示了一种定义 Guess
类型的方法,该类型只有在 new
函数接收到 1 到 100 之间的值时才会创建 Guess
的实例。
首先我们定义一个名为 Guess
的结构体,它有一个名为 value
的字段,该字段存储一个 i32
。这就是数字将被存储的地方。
然后我们在 Guess
上实现一个名为 new
的关联函数,用于创建 Guess
值的实例。new
函数定义为有一个名为 value
的参数,类型为 i32
,并返回一个 Guess
。new
函数体中的代码测试 value
以确保其在 1 到 100 之间。如果 value
未通过此测试,我们将调用 panic!
,这将提醒编写调用代码的程序员他们需要修复一个错误,因为使用此范围之外的 value
创建 Guess
会违反 Guess::new
依赖的契约。Guess::new
可能会触发 panic
的条件应在面向公众的 API 文档中讨论;我们将在第 14 章中介绍如何在你创建的 API 文档中使用文档约定来指示 panic!
的可能性。如果 value
通过了测试,我们将创建一个新的 Guess
,其 value
字段设置为 value
参数,并返回 Guess
。
接下来,我们实现一个名为value
的方法,该方法借用self
,没有其他参数,并返回一个i32
。这种类型的方法有时被称为getter,因为它的目的是从其字段中获取一些数据并返回。这个公共方法是必要的,因为Guess
结构体的value
字段是私有的。重要的是value
字段必须是私有的,这样使用Guess
结构体的代码就不能直接设置value
:模块外部的代码必须使用Guess::new
函数来创建Guess
的实例,从而确保没有一种方法可以让Guess
拥有一个未被Guess::new
函数中的条件检查过的value
。
一个函数如果其参数或返回值仅限于1到100之间的数字,那么可以在其签名中声明它接受或返回一个Guess
而不是一个i32
,并且不需要在其主体中进行任何额外的检查。
摘要
Rust 的错误处理特性旨在帮助您编写更健壮的代码。
The panic!
宏表示您的程序处于无法处理的状态,并
让您告诉进程停止,而不是尝试使用无效或
错误的值继续进行。The Result
枚举使用 Rust 的类型系统来表示
操作可能会失败,但您的代码可以从中恢复。您可以使用
Result
告诉调用您的代码的代码,它需要处理潜在的
成功或失败。在适当的情况下使用 panic!
和 Result
将使您的代码在面对不可避免的问题时更加可靠。
现在你已经看到了标准库如何使用泛型与Option
和Result
枚举,我们将讨论泛型的工作原理以及如何在你的代码中使用它们。