那么,我们该如何判断何时应当使用 panic!
宏,何时应当返回 Result
类型呢?当代码触发 panic!
时,它意味着程序将无法恢复。你可以认为 panic!
适用于那些你认为无法或无需恢复的错误情况,即你已经确定这个错误是不可恢复的。而当你选择返回 Result
类型时,你实际上是在给调用者提供选择权。调用者可以根据具体情况决定是尝试某种恢复策略,还是认为这个 Err
值代表了一个不可恢复的错误,进而选择调用 panic!
,将原本可能恢复的错误转变为不可恢复的错误。因此,在定义可能失败的函数时,默认返回 Result
类型是一个较好的选择。
在编写示例、原型代码和测试时,选择使用 panic
而不是返回 Result
往往更为合适。让我们探讨一下这样做的原因,然后讨论编译器无法判断失败是不可能的,但作为人类却可以知道的情况。本章节将总结一些关于如何在库代码中决定是否使用 panic
的一般性指导原则。
在撰写示例以阐释某些概念时,若包含过于复杂的错误处理代码,可能会使示例的主旨变得模糊。在这些示例中,对 unwrap
等方法的调用可能会导致 panic
,这可以理解为一种占位符,代表您期望应用程序处理错误的方式,而这些处理方式可能会随着代码的其他部分而有所变化。
同样,在您决定如何处理错误之前,unwrap
和 expect
方法在原型设计阶段非常方便。它们在代码中留下了清晰的标记,方便您在准备使程序更加健壮时进行相应的处理。
如果在测试过程中某个方法调用失败,我们希望整个测试也随之失败,哪怕这个方法并不是测试的主要目标。因为 panic!
是让测试被标记为失败的一种方式,所以在这种情况下,使用 unwrap
或 expect
来触发 panic
是恰当的做法。
当你确信某个 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
类型,编译器也会要求我们处理这个 Result
,因为它无法识别出这个字符串始终是一个有效的 IP 地址。这是因为编译器不具备足够的智能来识别硬编码字符串的合法性。
如果 IP 地址字符串是由用户提供的,那么确实存在失败的可能性,因此我们希望以更健壮的方式处理 Result
。在这种情况下,我们应该避免使用 unwrap
或 expect
,而是采用适当的错误处理机制,比如 match
或 if let
,来确保程序的健壮性。
如果将来我们需要从其他来源获取 IP 地址,那么提及 IP 地址是硬编码的假设将促使我们将 expect
替换为更完善的错误处理代码。这样做不仅有助于提高代码的可维护性,也使得代码更加灵活,能够适应未来可能的变化。
建议在代码可能处于错误状态时让代码 panic
。在此上下文中,不良状态指的是某些核心假设、保证、协定或不变量被破坏的情况,例如,将无效值、矛盾值或缺失值传递给代码。以下是一些具体的情况,其中使用 panic
可能是合适的:
当您的代码被他人调用,并且传入了无意义的值时,最佳的做法通常是返回一个错误(如果可能),这样库的用户就可以根据具体情况决定如何处理这些错误。这种做法提供了灵活性,允许调用者根据自己的需求来决定错误处理策略,比如记录错误、尝试替代方案或者向用户展示错误信息。
然而,在某些情况下,继续执行代码可能是不安全或有害的。例如,如果传入的值违反了程序的预期使用方式,或者会导致程序状态不一致,那么调用 panic!
可能是更好的选择。这样做可以立即停止程序的执行,防止可能的进一步损害,并提醒使用您库的人注意他们代码中的错误,以便在开发过程中进行修复。
同样,如果您的代码调用了外部代码,而这些外部代码超出了您的控制范围,并且返回了一个您无法修复的无效状态,那么使用 panic!
通常是合适的。这可以确保您的程序不会因为无效的状态而继续执行,从而避免潜在的错误扩散。
然而,当预期会出现失败时,返回一个 Result
比调用 panic!
更合适!示例包括给解析器提供格式错误的数据或HTTP请求返回表示已达到速率限制的状态。在这些情况下,返回一个Result
表明失败是一种预期的可能性,调用代码必须决定如何处理。
当您的代码执行某些操作时,如果使用无效的值调用这些操作可能会给用户带来风险,那么代码应该首先验证这些值的有效性。如果发现值无效,就应当触发异常。这主要是基于安全考虑:对无效数据进行操作可能会使您的代码面临安全漏洞的风险。例如,标准库会调用 panic!
来处理越界内存访问的尝试,因为访问不属于当前数据结构的内存是一个常见的安全问题。函数通常遵循一种“契约”:只有当输入满足特定条件时,它们的行为才得到保证。在违反这种契约时触发异常是合理的,因为这种违规总是表明调用方犯了错误,而且您不希望调用代码必须显式处理这种错误。实际上,对于这类错误,通常没有合理的恢复方法;需要编写代码的程序员来修复这个问题。函数的契约,尤其是当违反契约会导致异常时,应该在函数的API文档中得到清晰的说明。这样,调用者就能了解如何正确使用函数,并避免违反契约,从而减少程序出现安全问题的风险。
确实,在每个函数中都进行详尽的错误检查可能会使代码变得冗长且繁琐。不过,幸运的是,我们可以利用 Rust 的类型系统以及编译器自动完成的类型检查来简化这一过程。当函数接收到具有特定类型的参数时,你可以放心地继续处理代码逻辑,因为编译器已经确保了传入的值是有效的。例如,如果你的函数参数是一个非 Option
类型,那么程序期望接收到的是一个具体的值,而不是空值(None
)。在这种情况下,你的代码无需处理 Some
和 None
两种情况,因为它已经确定会有一个值。试图不传递任何值给这样的函数会导致编译错误,因此,函数在运行时无需检查这种情形。
另一个例子是使用无符号整数类型,比如 u32
,这确保了参数永远不可能是负数。通过这种方式,Rust 的类型系统帮助我们提前捕获错误,减少了运行时错误检查的需要,使得代码更加简洁和安全。
让我们深入探讨如何利用 Rust 的类型系统来确保我们得到的是一个有效的值,特别是通过创建一个自定义类型来进行验证。回想一下第 2 章中提到的猜谜游戏,我们的代码要求用户猜测一个介于 1 到 100 之间的数字。在与我们的秘密数字进行比较之前,我们并没有验证用户的猜测是否在这个范围内;我们只是确认了用户确实输入了一个猜测。在这种情况下,后果并不严重:即便用户的猜测超出了范围,我们的输出“太高”或“太低”仍然是准确的。然而,引导用户进行有效的猜测,并在用户输入超出范围的数字或输入字母等无效输入时有不同的反馈,这将是一个有益的功能增强。
为了实现这一点,我们可以创建一个自定义类型来确保用户的猜测始终有效。通过这种方式,我们可以在编译时就捕获潜在的错误,而不是在运行时。这样不仅提高了程序的健壮性,也提升了用户体验。接下来,我们可以探讨如何实现这样的自定义类型,并将其应用到猜谜游戏中,以确保用户输入的有效性。
一个改进的方法是将用户的猜测解析为 i32
类型而不是仅限于 u32
,这样可以允许负数的出现。然后,我们可以添加一个检查来确保数字落在指定的范围内。具体操作如下:
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
loop {
// --snip--
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
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--
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
if
表达式用于检查用户的猜测是否超出了预定的范围。如果猜测超出范围,程序会告知用户问题所在,并使用 continue
语句开始循环的下一次迭代,从而要求用户再次进行猜测。一旦通过了 if
表达式的检查,我们就可以确信 guess
的值位于1到100之间,之后便可以继续进行 guess
与秘密数字的比较。
然而,这种方法并非理想之选:如果程序绝对必须只处理1到100之间的值,并且存在许多具有这一要求的函数,那么在每个函数中都进行这样的检查将是繁琐的(同时可能对性能造成影响)。
相反,我们可以采用一种更优雅的解决方案:通过创建一个新的类型,并将验证逻辑封装在一个函数中,用于生成该类型的实例。这样,我们就避免了在代码的多个地方重复验证逻辑。具体来说,这个函数只有在接收到1到100之间的值时,才会创建并返回一个该类型的实例。这种方法使得函数可以在其签名中安全地使用这个新类型,并确信它们接收到的值总是有效的。下面的代码展示了如何定义这样一个Guess
类型,确保只有当新函数接收到1到100之间的值时,才会创建一个Guess
的实例。这种方法提高了代码的可维护性和效率。
#![allow(unused)]
fn 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
}
}
}
首先,我们定义了一个名为 Guess
的结构体,它包含一个名为 value
的字段,该字段用于存储一个 i32
类型的值,即用于保存猜测的数字。
接下来,我们在 Guess
结构体上实现一个名为 new
的关联函数,这个函数负责创建 Guess
实例。new
函数接受一个 i32
类型的参数,并返回一个 Guess
实例。在 new
函数的实现中,我们会对传入的 value
进行检查,确保它位于 1 到 100 的范围内。如果 value
未能通过这个检查,我们将触发 panic!
,以此提醒调用代码的程序员他们需要修复一个错误,因为任何超出此范围的 Guess
都会违反 Guess
类型的约定。Guess::new
可能触发 panic!
的情况应该在其公开的 API 文档中进行说明;我们将在后续的章节中介绍如何在 API 文档中标记可能触发 panic!
的情况。如果 value
通过了测试,我们将创建一个新的 Guess
实例,将其 value
字段设置为传入的 value
参数,并返回这个 Guess
实例。
接下来,我们为 Guess
结构体实现一个名为 value
的方法,这个方法借用 self
,不接受其他参数,并返回一个 i32
类型的值。这种方法通常被称为“getter”,因为它的作用是从结构体的字段中获取数据并返回。这个公共方法是必要的,因为 Guess
结构体的 value
字段是私有的。
value
字段必须是私有的,这样就不能允许使用 Guess
结构体的代码直接设置 value
:模块外部的代码必须通过 Guess::new
函数来创建 Guess
的实例。这样做可以确保 Guess
的 value
总是经过 Guess::new
中的条件检查,从而保证了 Guess
的 value
总是符合预期的范围。通过这种方式,我们确保了 Guess
类型的不变性,即任何 Guess
实例的 value
都在 1 到 100 之间。
确实,任何需要参数或仅返回1到100之间数字的函数,都可以在其签名中声明它接受或返回 Guess
类型,而不是 i32
。这样做的好处是,函数体内部就不需要执行任何额外的值有效性检查了。因为 Guess
类型本身就确保了其值在1到100的范围内,所以使用 Guess
类型的函数可以确信它们接收到的或返回的值总是有效的。这种类型安全的做法简化了代码,减少了错误处理的需要,并提高了程序的健壮性。
Rust 的错误处理机制旨在协助您编写更加健壮的代码。panic!
宏用于表明程序遇到了无法处理的情况,建议进程停止执行,而不是尝试使用无效或错误的值继续运行。而 Result
枚举则利用 Rust 的类型系统来表明某个操作可能失败,提供了一种方式让您的代码能够处理可能的成功或失败结果。您可以通过 Result
告知调用代码,它也需要准备处理潜在的成功或失败情况。
在适当的情境下使用 panic!
和 Result
,可以使您的代码在面对不可避免的问题时表现得更加可靠。使用 panic!
可以迅速响应严重错误,而使用 Result
则允许您优雅地处理预期内的错误情况,从而提高程序的整体稳定性和可靠性。
现在您已经了解了 Rust 标准库中 Option
和 Result
枚举如何利用泛型来提供灵活性和错误处理,我们可以深入探讨泛型的工作原理以及如何在您的代码中有效地使用它们。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。