错误处理指导原则
那么,该如何决定何时应该panic! 以及何时应该返回Result 呢?如果代码panic ,就没有恢复的可能。你可以选择对任何错误场景都调用panic! ,不管是否有可能恢复,不过这样就是你代替调用者决定了这是不可恢复的。选择返回Result 值的话,就将选择权交给了调用者,而不是代替他们做出决定。调用者可能会选择以符合他们场景的方式尝试恢复,或者也可能干脆就认为Err 是不可恢复的,所以他们也可能会调用panic! 并将可恢复的错误变成了不可恢复的错误。因此返回Result 是定义可能会失败的函数的一个好的默认选择。
有一些情况panic 比返回Result 更为合适,不过他们并不常见。让我们讨论一下为何在示例、代码原型和测试中,以及那些人们认为不会失败而编译器不这么看的情况下,panic 是合适的。在当有可能会导致有害状态的情况下建议使用 panic!
,在这里,有害状态是指当一些假设、保证、协议或不可变性被打破的状态,例如无效的值、自相矛盾的值或者被传递了不存在的值。外加如下几种情况:
有害状态并不包含预期会偶尔发生的错误
在此之后代码的运行依赖于不处于这种有害状态
当没有可行的手段来将有害状态信息编码进所使用的类型中的情况
如果别人调用你的代码并传递了一个没有意义的值,最好的情况也许就是 panic!
并警告使用你的库的人他的代码中有bug 以便他能在开发时就修复它。类似的,panic!
通常适合调用不能够控制的外部代码时,这时无法修复其返回的无效状态。
然而当错误预期会出现时,返回 Result
仍要比调用 panic!
更为合适。这样的例子包括解析器接收到格式错误的数据,或者HTTP 请求返回了一个表明触发了限流的状态。在这些例子中,应该通过返回 Result
来表明失败预期是可能的,这样将有害状态向上传播,调用者就可以决定该如何处理这个问题。使用 panic!
来处理这些情况就不是最好的选择。
当代码对值进行操作时,应该首先验证值是有效的,并在其无效时 panic!
。这主要是出于安全的原因:尝试操作无效数据会暴露代码漏洞,这就是标准库在尝试越界访问数组时会 panic!
的主要原因:尝试访问不属于当前数据结构的内存是一个常见的安全隐患。函数通常都遵循契约(contracts ) :他们的行为只有在输入满足特定条件时才能得到保证。当违反契约时panic 是有道理的,因为这通常代表调用方的bug ,而且这也不是那种你希望所调用的代码必须处理的错误。事实上所调用的代码也没有合理的方式来恢复,而是需要调用方的程序员修复其代码。函数的契约,尤其是当违反它会造成panic 的契约,应该在函数的API 文档中得到解释。
虽然在所有函数中都拥有许多错误检查是冗长而烦人的。幸运的是,可以利用Rust 的类型系统(以及编译器的类型检查)为你进行很多检查。如果函数有一个特定类型的参数,可以在知晓编译器已经确保其拥有一个有效值的前提下进行你的代码逻辑。例如,如果你使用了一个并不是 Option
的类型,则程序期望它是 有值 的并且不是 空值 。你的代码无需处理 Some
和 None
这两种情况,它只会有一种情况就是绝对会有一个值。尝试向函数传递空值的代码甚至根本不能编译,所以你的函数在运行时没有必要判空。另外一个例子是使用像 u32
这样的无符号整型,也会确保它永远不为负。
创建自定义类型进行有效性验证
让我们使用Rust 类型系统的思想来进一步确保值的有效性,并尝试创建一个自定义类型以进行验证。回忆一下第二章的猜猜看游戏,我们的代码要求用户猜测一个1 到100 之间的数字,在将其与秘密数字做比较之前我们从未验证用户的猜测是位于这两个数字之间的,我们只验证它是否为正。在这种情况下,其影响并不是很严重: “Too high” 或 “Too low” 的输出仍然是正确的。但是这是一个很好的引导用户得出有效猜测的辅助,例如当用户猜测一个超出范围的数字或者输入字母时采取不同的行为。
一种实现方式是将猜测解析成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 在1 到100 之间的情况下与秘密数字作比较了。然而,这并不是一个理想的解决方案:如果让程序仅仅处理1 到100 之间的值是一个绝对需要满足的要求,而且程序中的很多函数都有这样的要求,在每个函数中都有这样的检查将是非常冗余的(并可能潜在的影响性能) 。
相反我们可以创建一个新类型来将验证放入创建其实例的函数中,而不是到处重复这些检查。这样就可以安全的在函数签名中使用新类型并相信他们接收到的值。示例9-10 中展示了一个定义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
}
}
首先,我们定义了一个包含i32 类型字段value 的结构体Guess 。这里是储存猜测值的地方。接着在Guess 上实现了一个叫做new 的关联函数来创建Guess 的实例。new 定义为接收一个i32 类型的参数value 并返回一个Guess 。new 函数中代码的测试确保了其值是在1 到100 之间的。如果value 没有通过测试则调用panic! ,这会警告调用这个函数的程序员有一个需要修改的bug ,因为创建一个value 超出范围的Guess 将会违反Guess::new 所遵循的契约。Guess::new 会出现panic 的条件应该在其公有API 文档中被提及;第十四章会涉及到在API 文档中表明panic! 可能性的相关规则。如果value 通过了测试,我们新建一个Guess ,其字段value 将被设置为参数value 的值,接着返回这个Guess 。
接着,我们实现了一个借用了self 的方法value ,它没有任何其他参数并返回一个i32 。这类方法有时被称为getter ,因为它的目的就是返回对应字段的数据。这样的公有方法是必要的,因为Guess 结构体的value 字段是私有的。私有的字段value 是很重要的,这样使用Guess 结构体的代码将不允许直接设置value 的值:调用者 必须 使用Guess::new 方法来创建一个Guess 的实例,这就确保了不会存在一个value 没有通过Guess::new 函数的条件检查的Guess 。于是,一个接收(或返回)1 到100 之间数字的函数就可以声明为接收(或返回)Guess 的实例,而不是i32 ,同时其函数体中也无需进行任何额外的检查。