panic!或不panic!

那么,您如何决定何时应该调用panic!以及您应该何时返回Result?当代码出现 panic 时,无法恢复。您可以调用panic!对于任何错误情况,无论是否有可能的恢复方法,但是 那么你正在做出一个决定,即情况是无法恢复的 调用代码。当您选择返回Result值,则为 调用代码选项。调用代码可以选择尝试在 方式,或者它可以决定Errvalue 是不可恢复的,因此它可以调用panic!并转动你的 可恢复的错误转换为不可恢复的错误。因此,返回Result是一个 当您定义可能失败的函数时,这是 Good default 选择。

在示例、原型代码和测试等情况下,它更多 适合编写 panic 的代码,而不是返回Result.让我们 探索原因,然后讨论编译器无法判断的情况 失败是不可能的,但你作为一个人可以。本章将以 有关如何决定是否在库代码中 panic 的一些通用准则。

示例、原型代码和测试

当您编写示例来说明某些概念时,还包括 健壮的错误处理代码可能会使示例不那么清晰。在示例中,它是 理解对unwrap那 could panic 的意思是 placeholder 替换为您希望应用程序处理错误的方式,这可以 根据代码的其余部分执行的作而有所不同。

同样,unwrapexpect方法在原型设计时非常方便, 在您准备好决定如何处理错误之前。他们在 当您准备好使程序更加健壮时的代码。

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

您拥有的信息比编译器多的情况

调用unwrapexpect当你有一些 其他逻辑,确保Result将具有Ok值,但 logic 不是编译器理解的。您仍然会有一个Result价值 ,您需要处理:无论您调用什么作,它仍然具有 一般情况下失败的可能性,即使这在逻辑上是不可能的 您的特定情况。如果可以通过手动检查代码来确保 你永远不会有Err变体,则完全可以调用unwrap,最好记录您认为您永远不会拥有Err变体expect发短信。下面是一个示例:

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

    let home: IpAddr = "127.0.0.1"
        .parse()
        .expect("Hardcoded IP address should be valid");
}

我们正在创建一个IpAddrinstance 来解析硬编码字符串。我们可以看到 那127.0.0.1是有效的 IP 地址,因此可以使用expect这里。但是,具有硬编码的有效字符串不会更改返回类型 的parse方法:我们仍然得到一个Result值,编译器将 仍然让我们处理Result就好像Errvariant 是一种可能性 因为编译器不够聪明,无法看到这个字符串始终是一个 有效的 IP 地址。如果 IP 地址字符串来自用户,而不是 硬编码到程序中,因此确实有失败的可能性, 我们肯定希望处理Result相反,以更强大的方式。 提及此 IP 地址是硬编码的假设将提示我们 改变expect以更好地处理代码,如果将来我们需要获取 来自其他来源的 IP 地址。

错误处理准则

建议在代码可能 panic 时让代码 panic 最终处于糟糕的状态。在这种情况下,不良状态是指当某些假设 保证、合约或不变量已被破坏,例如当无效值、 将矛盾值或缺失值传递给您的代码 - 加 1 或 更多以下内容:

  • 不良状态是出乎意料的,而不是 可能偶尔会发生,例如用户在错误的 格式。
  • 此之后的代码需要依赖于不处于这种 bad 状态, 而不是在每一步都检查问题。
  • 没有一种将此信息编码为你使用的类型的好方法。我们将 通过一个示例来说明我们在 “编码状态和行为” 中的含义 as Types“部分。

如果有人调用您的代码并传入没有意义的值,则 如果可以,最好返回一个错误,以便库的用户可以决定什么 他们想在那种情况下这样做。但是,在继续可能 不安全或有害,最好的选择可能是调用panic!并提醒 使用库的人来修复他们代码中的 bug,以便他们可以在 发展。同样地panic!如果您调用 外部代码,并且它返回一个无效状态,该状态 你没有办法解决。

但是,当预期会失败时,返回Result而不是使panic!叫。示例包括给定格式错误的解析器 data 或 HTTP 请求返回指示您已达到某个速率的状态 限制。在这些情况下,返回Result表示 failure 是 调用代码必须决定如何处理的预期可能性。

当您的代码执行可能使用户处于风险中的作时,如果它是 使用无效值调用,则代码应首先验证值是否有效 如果值无效,则 panic。这主要是出于安全原因: 尝试对无效数据进行作可能会使您的代码暴露于漏洞中。 这是标准库调用panic!如果您尝试 越界内存访问:尝试访问不属于 当前数据结构是一个常见的安全问题。函数通常具有 contract:只有当 inputs 满足特定的 要求。在违反合同时惊慌是有道理的,因为 Contract Violation 始终表示调用方 bug ,它不是一种 错误。事实上,有 没有合理的方法来调用代码进行恢复;调用程序员需要 以修复代码。函数的协定,尤其是当违规将 导致 panic,应在函数的 API 文档中解释。

但是,在所有函数中都有大量错误检查会很冗长 而且很烦人。幸运的是,你可以使用 Rust 的类型系统(以及 checking done by the compiler)为你做很多检查。如果您的 function 具有特定类型作为参数,您可以继续执行代码的 logic 知道编译器已经确保您有一个有效的值。为 例如,如果你有一个 type 而不是Option,您的程序期望 有什么都没有。然后,您的代码不必处理 两种情况SomeNonevariants:它只有一个 case 绝对有价值。尝试不向函数传递任何内容的代码不会 甚至编译,因此您的函数不必在运行时检查该情况。 另一个示例是使用无符号整数类型,例如u32,这可确保 该参数永远不会为负数。

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

让我们考虑使用 Rust 的类型系统来确保我们有一个有效的值 更进一步,看看如何创建自定义类型进行验证。回想一下 第 2 章中的猜谜游戏,其中我们的代码要求用户猜一个数字 介于 1 和 100 之间。我们从未验证过用户的猜测是否介于两者之间 号码,然后与我们的秘密号码进行核对;我们只验证了这一点 猜测是肯定的。在这种情况下,后果并不是很可怕:我们的 “Too high” 或 “Too Low” 的输出仍然是正确的。但这将是一个 有用的增强功能,可指导用户进行有效猜测,并具有不同的 当用户猜出超出范围的数字时的行为与 例如,用户键入 letters 来代替。

一种方法是将猜测解析为i32而不仅仅是u32以允许可能的负数,然后添加对 number 在 RANGE 中,如下所示:

文件名: src/main.rs

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

ifexpression 检查我们的值是否超出范围,告诉用户 关于问题,并调用continue开始循环的下一次迭代 并要求再次猜测。在if表达式,我们可以继续执行 比较guess和秘密号码知道guess是 介于 1 和 100 之间。

但是,这并不是一个理想的解决方案:如果 程序仅对 1 到 100 之间的值进行作,并且它有很多函数 有了这个要求,在每个函数中都有这样的检查将是 繁琐的(并且可能会影响性能)。

相反,我们可以创建一个新类型并将验证放在一个函数中以创建 类型的实例,而不是到处重复验证。那 方式,函数可以安全地在其签名中使用新类型,并且 自信地使用他们收到的值。示例 9-13 展示了定义Guesstype 的 ID 实例,它只会创建Guess如果new功能 接收介于 1 和 100 之间的值。

文件名: src/lib.rs

#![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
    }
}
}

示例 9-13:一个Guesstype 的 介于 1 和 100 之间的值

首先,我们定义一个名为Guess,它有一个名为value那 持有i32.这是存储号码的地方。

然后我们实现一个名为newGuess创建 的实例Guess值。这newfunction 被定义为具有一个 参数名称value的类型i32并返回一个Guess.代码中的 body 的new功能测试value以确保它介于 1 和 100 之间。 如果value未通过此测试,我们创建一个panic!call 调用,它会提醒 编写调用代码的程序员,他们有他们需要的 bug 来修复,因为创建一个Guess替换为value超出此范围将 违反合同Guess::new是依赖的。其中Guess::new可能会恐慌 应该在其面向公众的 API 中讨论 文档;我们将介绍表明可能性的文档约定 的panic!在第 14 章中创建的 API 文档中。如果value通过测试,我们创建一个新的Guess及其value字段集 到value参数并返回Guess.

接下来,我们实现一个名为value那个借用self,没有任何 other 参数,并返回一个i32.这种方法有时称为 一个 getter,因为它的目的是从其 fields 中获取一些数据并返回 它。此公共方法是必需的,因为value字段的Guessstruct 是私有的。重要的是,value字段为私有 so 代码 使用Guessstruct 不允许设置value直接:外部代码 该模块必须使用Guess::new函数创建Guess,从而确保没有办法使用Guess以具有value那 未被Guess::new功能。

具有参数或仅返回 1 到 100 之间的数字的函数可以 然后在其签名中声明它接受或返回一个Guess而不是i32并且不需要在其正文中执行任何其他检查。

总结

Rust 的错误处理功能旨在帮助您编写更健壮的代码。 这panic!宏表示您的程序处于无法处理的状态,并且 用于指示进程停止,而不是尝试使用 Invalid 或 不正确的值。这Resultenum 使用 Rust 的类型系统来表示 作可能会以代码可以从中恢复的方式失败。您可以使用Result告诉调用代码的代码它需要处理潜在的 成功或失败也是如此。用panic!Result在适当的 情况会让你的代码在面对不可避免的问题时更加可靠。

现在,您已经了解了标准库使用 generics 的有用方法 这OptionResultenums,我们将讨论泛型的工作原理以及您 可以在您的代码中使用它们。

本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准