Rust之错误处理(三):panic!还是不要panic!

开发环境

  • Windows 10
  • Rust 1.68.0

 

  •   VS Code 1.76.2

项目工程

这里继续沿用上次工程rust-demo

panic!还是不要panic! 

那么,你如何决定何时应该调用panic!以及何时应该返回Result?当代码失败时,没有办法恢复。你可以在任何错误情况下调用panic!,无论是否有可能恢复,但你是代表调用代码来决定一个情况是不可恢复的。当你选择返回一个Result值时,你给了调用代码一些选择。调用代码可以选择尝试以适合其情况的方式进行恢复,或者它可以决定这种情况下的Err值是不可恢复的,所以它可以调用panic!并将你的可恢复错误变成不可恢复的错误。因此,当你定义一个可能失败的函数时,返回 Result是一个很好的默认选择。

在诸如例子、原型代码和测试等情况下,写出崩溃而不是返回Result的代码更为合适。让我们来探讨一下原因,然后讨论在哪些情况下编译器无法判断失败是不可能的,但作为人类的你可以。本章最后将介绍一些关于如何决定是否在库代码中进行恐慌的一般准则。

示例、原型代码和测试

当你写一个例子来说明一些概念时,也包括健壮的错误处理代码会使这个例子不那么清晰。在例子中,我们可以理解为,对像unwrap这样的方法的调用可能会引起恐慌,这是你希望你的应用程序处理错误的方式的一个占位符,它可以根据你的其他代码的做法而有所不同。 

同样,在你准备决定如何处理错误之前,unwrapexpect方法在原型设计时是非常方便的。它们在你的代码中留下了清晰的标记,以便你准备使你的程序更加健壮。

如果在测试中一个方法调用失败,你会希望整个测试都失败,即使这个方法不是被测试的功能。因为panic!是一个测试被标记为失败的方式,调用unwrapexpect正是应该发生的。

你比编译器拥有更多信息的案例 

当你有一些其他的逻辑来确保结果会有一个Ok值时,调用unwrapexpect也是合适的,但这个逻辑不是编译器所能理解的东西。你仍然有一个需要处理的Result值:无论你调用的是什么操作,在一般情况下仍然有失败的可能,尽管在你的特定情况下逻辑上是不可能的。如果你能通过手动检查代码来确保永远不会出现Err变量,那么调用unwrap是完全可以接受的,甚至最好在expect文本中记录你认为永远不会出现Err变量的原因。这里有一个例子。 

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当作Err变量来处理,因为编译器还没有聪明到可以看出这个字符串总是一个有效的IP地址。如果这个IP地址字符串来自用户,而不是硬编码到程序中,因此确实有失败的可能,我们肯定会想用一种更稳健的方式来处理Result。提到这个IP地址是硬编码的假设将促使我们改变expect,以更好地处理错误的代码,如果将来我们需要从其他来源获得IP地址的话。 

错误处理指南

当你的代码有可能结束在一个坏的状态时,建议让你的代码恐慌。在这里,坏状态是指一些假设、保证、契约或不变性被破坏,比如无效的值、矛盾的值或缺失的值被传递给你的代码,再加上以下一个或多个。

  • 不良的状态是出乎意料的事情,而不是可能偶尔发生的事情,比如用户以错误的格式输入数据。
  • 你的代码在这一点上需要依靠不处于这种坏状态,而不是在每一步都检查问题。
  • 在你使用的类型中没有一个好的方法来编码这些信息。我们将在第后面章节的 "将状态和行为编码为类型 "一节中通过一个例子来说明我们的意思。

如果有人调用你的代码并传入一些不合理的值,如果可以的话,最好是返回一个错误,这样库的用户就可以决定在这种情况下他们要做什么。然而,在继续可能是不安全的或有害的情况下,最好的选择可能是调用panic!并提醒使用你的库的人注意他们代码中的错误,这样他们就可以在开发过程中修复它。同样,如果你正在调用不受你控制的外部代码,并且它返回了一个你无法修复的无效状态,那么panic!往往是合适的。

然而,当失败是预料之中的时候,返回一个Result比调用一个panic!更合适。这方面的例子包括解析器得到了畸形的数据,或者HTTP请求返回的状态表明你已经达到了速率限制。在这些情况下,返回一个Result表明失败是一种预期的可能性,调用代码必须决定如何处理。

当你的代码执行一个操作时,如果使用无效的值调用,可能会使用户处于危险之中,你的代码应该首先验证这些值是否有效,如果值无效,就应该崩溃。这主要是出于安全考虑:试图对无效的数据进行操作会使你的代码暴露在漏洞中。这也是标准库在你试图进行越界内存访问时会调用panic!的主要原因:试图访问不属于当前数据结构的内存是一个常见的安全问题。函数通常有约定:只有当输入满足特定要求时,它们的行为才会得到保证。当约定被违反时,程序崩溃是有道理的,因为违反约定总是表明有一个调用方的错误,这不是一种你希望调用代码必须明确处理的错误。事实上,调用代码没有合理的方法来恢复;调用程序员需要修复代码。一个函数的约定,特别是当违反约定会导致崩溃时,应该在该函数的API文档中加以解释。

然而,在你所有的函数中都有大量的错误检查将是冗长和令人讨厌的。幸运的是,你可以使用Rust的类型系统(以及由编译器完成的类型检查)来为你做许多检查。如果你的函数有一个特定的类型作为参数,你可以继续你的代码逻辑,因为你知道编译器已经确保了你有一个有效的值。例如,如果你有一个类型而不是一个Option,你的程序就会期望有一个东西而不是什么都没有。那么你的代码就不需要处理SomeNone变体的两种情况:它只需要处理一个肯定有值的情况。试图向你的函数传递无的代码甚至不会被编译,所以你的函数在运行时不需要检查这种情况。另一个例子是使用无符号的整数类型,如u32,这样可以确保参数永远不会是负数。

创建用于验证的自定义类型

让我们把使用Rust的类型系统来确保我们有一个有效的值的想法更进一步,看看如何创建一个自定义类型来进行验证。回顾一下之前章节中的猜谜游戏,我们的代码要求用户在1到100之间猜一个数字。在与我们的秘密数字进行核对之前,我们从未验证过用户的猜测是否在这些数字之间;我们只验证了猜测是肯定的。在这种情况下,后果不是很严重:我们输出的 "太高 "或 "太低 "仍然是正确的。但如果能引导用户进行有效的猜测,并在用户猜测出一个超出范围的数字时与用户输入字母时有不同的行为,这将是一个有用的改进。

一种方法是将猜测解析为i32,而不仅仅是u32,以允许潜在的负数,然后添加一个数字是否在范围内的检查,像这样。

loop {
    // --片段--

    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) {
}

if表达式检查我们的值是否超出了范围,告诉用户这个问题,并调用continue来开始下一次循环的迭代,并要求再次进行猜测。在if表达式之后,我们可以继续进行guess和秘密数字之间的比较,因为我们知道guess在1到100之间。

然而,这并不是一个理想的解决方案:如果程序只对1到100之间的数值进行操作是绝对关键的,而且它有许多函数都有这样的要求,在每个函数中都有这样的检查是很乏味的(而且可能影响性能)。

相反,我们可以建立一个新的类型,并将验证放在一个函数中,以创建该类型的实例,而不是到处重复验证。这样一来,函数就可以安全地在签名中使用新类型,并自信地使用它们收到的值。下例例13显示了一种定义Guess类型的方法,只有当new函数收到1到100之间的值时,才会创建一个Guess的实例。

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
    }
}

例13

首先,我们定义了一个名为Guess的结构,该结构有一个名为value的字段,存放着一个i32。这就是数字将被存储的地方。

然后,我们在Guess上实现一个名为new的关联函数,创建Guess值的实例。new函数被定义为有一个i32类型的参数名为value,并返回一个Guess。新函数正文中的代码测试value以确保它在1到100之间。如果value没有通过这个测试,我们就会发出一个panic!调用,这将提醒正在编写调用代码的程序员,他们有一个需要修复的错误,因为用这个范围以外的值创建一个Guess将违反Guess::new所依赖的约定。Guess::new可能发生崩溃的条件应该在其面向公众的API文档中讨论;我们将在后面章节中介绍表明崩溃可能性的文档惯例!在你创建的API文档中。如果value确实通过了测试,我们就创建一个新的Guess,其value字段设置为value参数并返回Guess

接下来,我们实现一个名为value的方法,它借用了self,没有任何其他参数,并返回一个i32。这种方法有时被称为getter,因为它的目的是从其字段中获取一些数据并返回。这种公共方法是必要的,因为Guess结构的value域是私有的。重要的是,value域是私有的,所以使用Guess结构的代码不允许直接设置value:模块外的代码必须使用Guess::new函数来创建Guess的实例,从而确保Guess没有办法拥有一个未经Guess::new函数检查的值。 

一个有参数或只返回1到100之间的数字的函数可以在其签名中声明它接收或返回一个Guess而不是一个i32,并且不需要在其主体中做任何额外的检查。

总结

Rust的错误处理功能是为了帮助你写出更健壮的代码。panic!宏表示你的程序处于它无法处理的状态,并让你告诉进程停止,而不是试图用无效或不正确的值继续进行。Result枚举使用Rust的类型系统来表示操作可能会失败,而你的代码可以从中恢复。你可以使用Result来告诉调用你的代码的代码,它也需要处理潜在的成功或失败。在适当的情况下使用panic!Result将使你的代码在面对不可避免的问题时更加可靠。

现在你已经看到了标准库用OptionResult枚举使用泛型的有用方法,我们将讨论泛型是如何工作的以及你如何在你的代码中使用它们。

本章重点

  • panic!还是不panic!的原则
  • unwrap或expect的使用方法和注意细节
  • 错误处理的指导步骤
  • 如何创建用于验证的自定义类型
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值