Update ch09-03-to-panic-or-not-to-panic.md

pull/425/head
Nango 4 years ago committed by GitHub
parent ccd2c0ae0e
commit b7b5629fd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -14,7 +14,7 @@
类似地,在我们准备好决定如何处理错误之前,`unwrap`和`expect`方法在原型设计时非常方便。当我们准备好让程序更加健壮时,它们会在代码中留下清晰的标记。 类似地,在我们准备好决定如何处理错误之前,`unwrap`和`expect`方法在原型设计时非常方便。当我们准备好让程序更加健壮时,它们会在代码中留下清晰的标记。
如果方法调用在测试中失败了,我们希望这个测试都失败,即便这个方法并不是需要测试的功能。因为 `panic!` 是测试如何被标记为失败的,调用 `unwrap``expect` 就是应该发生的事情 如果方法调用在测试中失败了,我们希望这个测试都失败,即便这个方法并不是需要测试的功能。因为 `panic!` 会将测试标记为失败,此时调用 `unwrap``expect` 是恰当的
### 当我们比编译器知道更多的情况 ### 当我们比编译器知道更多的情况
@ -26,7 +26,7 @@ use std::net::IpAddr;
let home: IpAddr = "127.0.0.1".parse().unwrap(); let home: IpAddr = "127.0.0.1".parse().unwrap();
``` ```
我们通过解析一个硬编码的字符来创建一个 `IpAddr` 实例。可以看出 `127.0.0.1` 是一个有效的 IP 地址,所以这里使用 `unwrap` 是可以接受的。然而,拥有一个硬编码的有效的字符串也不能改变 `parse` 方法的返回值类型:它仍然是一个 `Result` 值,而编译器仍然就好像还是有可能出现 `Err` 成员那样要求我们处理 `Result`因为编译器还没有智能到可以识别出这个字符串总是一个有效的 IP 地址。如果 IP 地址字符串来源于用户而不是硬编码进程序中的话,那么就 **确实** 有失败的可能性,这时就绝对需要我们以一种更健壮的方式处理 `Result` 了。 我们通过解析一个硬编码的字符来创建一个 `IpAddr` 实例。可以看出 `127.0.0.1` 是一个有效的 IP 地址,所以这里使用 `unwrap` 是可以接受的。然而,拥有一个硬编码的有效的字符串也不能改变 `parse` 方法的返回值类型:它仍然是一个 `Result` 值,而编译器仍然会要求我们处理这个 `Result`,好像还是有可能出现 `Err` 成员那样。这是因为编译器还没有智能到可以识别出这个字符串总是一个有效的 IP 地址。如果 IP 地址字符串来源于用户而不是硬编码进程序中的话,那么就 **确实** 有失败的可能性,这时就绝对需要我们以一种更健壮的方式处理 `Result` 了。
### 错误处理指导原则 ### 错误处理指导原则
@ -38,11 +38,11 @@ let home: IpAddr = "127.0.0.1".parse().unwrap();
如果别人调用你的代码并传递了一个没有意义的值,最好的情况也许就是 `panic!` 并警告使用你的库的人他的代码中有 bug 以便他能在开发时就修复它。类似的,`panic!` 通常适合调用不能够控制的外部代码时,这时无法修复其返回的无效状态。 如果别人调用你的代码并传递了一个没有意义的值,最好的情况也许就是 `panic!` 并警告使用你的库的人他的代码中有 bug 以便他能在开发时就修复它。类似的,`panic!` 通常适合调用不能够控制的外部代码时,这时无法修复其返回的无效状态。
然而当错误预期会出现时,返回 `Result` 仍要比调用 `panic!` 更为合适。这样的例子包括解析器接收到错误数据,或者 HTTP 请求返回一个表明触发了限流的状态。在这些例子中,应该通过返回 `Result` 来表明失败预期是可能的,这样将有害状态向上传播,调用者就可以决定该如何处理这个问题。使用 `panic!` 来处理这些情况就不是最好的选择。 然而当错误预期会出现时,返回 `Result` 仍要比调用 `panic!` 更为合适。这样的例子包括解析器接收到格式错误数据,或者 HTTP 请求返回一个表明触发了限流的状态。在这些例子中,应该通过返回 `Result` 来表明失败预期是可能的,这样将有害状态向上传播,调用者就可以决定该如何处理这个问题。使用 `panic!` 来处理这些情况就不是最好的选择。
当代码对值进行操作时,应该首先验证值是有效的,并在其无效时 `panic!`。这主要是出于安全的原因:尝试操作无效数据会暴露代码漏洞,这就是标准库在尝试越界访问数组时会 `panic!` 的主要原因:尝试访问不属于当前数据结构的内存是一个常见的安全隐患。函数通常都遵循 **契约***contracts*):他们的行为只有在输入满足特定条件时才能得到保证。当违反契约时 panic 是有道理的,因为这通常代表调用方的 bug而且这也不是那种你希望调用方必须处理的错误。事实上也没有合理的方式来恢复调用方的代码:调用方的 **程序员** 需要修复其代码。函数的契约,尤其是当违反它会造成 panic 的契约,应该在函数的 API 文档中得到解释。 当代码对值进行操作时,应该首先验证值是有效的,并在其无效时 `panic!`。这主要是出于安全的原因:尝试操作无效数据会暴露代码漏洞,这就是标准库在尝试越界访问数组时会 `panic!` 的主要原因:尝试访问不属于当前数据结构的内存是一个常见的安全隐患。函数通常都遵循 **契约***contracts*):他们的行为只有在输入满足特定条件时才能得到保证。当违反契约时 panic 是有道理的,因为这通常代表调用方的 bug而且这也不是那种你希望所调用的代码必须处理的错误。事实上所调用的代码也没有合理的方式来恢复,而是需要调用方的 **程序员** 修复其代码。函数的契约,尤其是当违反它会造成 panic 的契约,应该在函数的 API 文档中得到解释。
虽然在所有函数中都拥有许多错误检查是冗长而烦人的。幸运的是,可以利用 Rust 的类型系统(以及编译器的类型检查)为你进行很多检查。如果函数有一个特定类型的参数,可以在知晓编译器已经确保其拥有一个有效值的前提下进行你的代码逻辑。例如,如果你使用了一个不同于 `Option` 的类型,而且程序期望它是 **有值** 的并且不是 **空值**。你的代码无需处理 `Some``None` 这两种情况,它只会有一种情况就是绝对会有一个值。尝试向函数传递空值的代码甚至根本不能编译,所以你的函数在运行时没有必要判空。另外一个例子是使用像 `u32` 这样的无符号整型,也会确保它永远不为负。 虽然在所有函数中都拥有许多错误检查是冗长而烦人的。幸运的是,可以利用 Rust 的类型系统(以及编译器的类型检查)为你进行很多检查。如果函数有一个特定类型的参数,可以在知晓编译器已经确保其拥有一个有效值的前提下进行你的代码逻辑。例如,如果你使用了一个并不是 `Option` 的类型,则程序期望它是 **有值** 的并且不是 **空值**。你的代码无需处理 `Some``None` 这两种情况,它只会有一种情况就是绝对会有一个值。尝试向函数传递空值的代码甚至根本不能编译,所以你的函数在运行时没有必要判空。另外一个例子是使用像 `u32` 这样的无符号整型,也会确保它永远不为负。
### 创建自定义类型进行有效性验证 ### 创建自定义类型进行有效性验证
@ -71,7 +71,7 @@ loop {
`if` 表达式检查了值是否超出范围,告诉用户出了什么问题,并调用 `continue` 开始下一次循环,请求另一个猜测。`if` 表达式之后,就可以在知道 `guess` 在 1 到 100 之间的情况下与秘密数字作比较了。 `if` 表达式检查了值是否超出范围,告诉用户出了什么问题,并调用 `continue` 开始下一次循环,请求另一个猜测。`if` 表达式之后,就可以在知道 `guess` 在 1 到 100 之间的情况下与秘密数字作比较了。
然而,这并不是一个理想的解决方案:程序只处理 1 到 100 之间的值是绝对不可取的,而且如果有很多函数都有这样的要求,在每个函数中都有这样的检查将是非常冗余的(并可能潜在的影响性能)。 然而,这并不是一个理想的解决方案:如果让程序仅仅处理 1 到 100 之间的值是一个绝对需要满足的要求,而且程序中的很多函数都有这样的要求,在每个函数中都有这样的检查将是非常冗余的(并可能潜在的影响性能)。
相反我们可以创建一个新类型来将验证放入创建其实例的函数中,而不是到处重复这些检查。这样就可以安全的在函数签名中使用新类型并相信他们接收到的值。示例 9-10 中展示了一个定义 `Guess` 类型的方法,只有在 `new` 函数接收到 1 到 100 之间的值时才会创建 `Guess` 的实例: 相反我们可以创建一个新类型来将验证放入创建其实例的函数中,而不是到处重复这些检查。这样就可以安全的在函数签名中使用新类型并相信他们接收到的值。示例 9-10 中展示了一个定义 `Guess` 类型的方法,只有在 `new` 函数接收到 1 到 100 之间的值时才会创建 `Guess` 的实例:
@ -105,7 +105,7 @@ impl Guess {
接着,我们实现了一个借用了 `self` 的方法 `value`,它没有任何其他参数并返回一个 `i32`。这类方法有时被称为 *getter*,因为它的目的就是返回对应字段的数据。这样的公有方法是必要的,因为 `Guess` 结构体的 `value` 字段是私有的。私有的字段 `value` 是很重要的,这样使用 `Guess` 结构体的代码将不允许直接设置 `value` 的值:调用者 **必须** 使用 `Guess::new` 方法来创建一个 `Guess` 的实例,这就确保了不会存在一个 `value` 没有通过 `Guess::new` 函数的条件检查的 `Guess` 接着,我们实现了一个借用了 `self` 的方法 `value`,它没有任何其他参数并返回一个 `i32`。这类方法有时被称为 *getter*,因为它的目的就是返回对应字段的数据。这样的公有方法是必要的,因为 `Guess` 结构体的 `value` 字段是私有的。私有的字段 `value` 是很重要的,这样使用 `Guess` 结构体的代码将不允许直接设置 `value` 的值:调用者 **必须** 使用 `Guess::new` 方法来创建一个 `Guess` 的实例,这就确保了不会存在一个 `value` 没有通过 `Guess::new` 函数的条件检查的 `Guess`
如此获取一个参数并只返回 1 到 100 之间数字的函数就可以声明为获取或返回一个 `Guess`,而不是 `i32`,同时其函数体中也无需进行任何额外的检查。 如此获取一个参数并只返回 1 到 100 之间数字的函数就可以声明为接收或返回一个 `Guess`,而不是 `i32`,同时其函数体中也无需进行任何额外的检查。
## 总结 ## 总结

Loading…
Cancel
Save