可恢复的错误与 Result
大多数错误并没有严重到需要程序完全停止的程度。有时,当一个函数失败时,它的原因是你可以很容易地解释和应对的。例如,如果你试图将两个大的整数相加,而操作溢出,因为总和超过了最大的可表示值,你可能想返回一个错误或一个包装好的结果,而不是引起未定义行为或终止程序。
Result
枚举
回顾第8章中的“通用数据类型” 一节中,Result
枚举被定义为具有两个变体,即 Ok
和 Err
,如下所示:
enum Result<T, E> {
Ok: T,
Err: E,
}
Result<T, E>
枚举有两个泛型类型,T
和E
,以及两个成员:Ok
,存放T
类型的值,Err
,存放E
类型的值。这个定义使得我们可以在任何地方使用Result
枚举,该操作可能成功(返回T
类型的值)或失败(返回E
类型的值)。
ResultTrait
ResultTrait
trait提供了处理Result<T, E>
枚举的方法,例如解包值,检查Result
是Ok
还是Err
,以及用自定义的消息进行panic。ResultTraitImpl
实现定义了这些方法的逻辑。
trait ResultTrait<T, E> {
fn expect<+Drop<E>>(self: Result<T, E>, err: felt252) -> T;
fn unwrap<+Drop<E>>(self: Result<T, E>) -> T;
fn expect_err<+Drop<T>>(self: Result<T, E>, err: felt252) -> E;
fn unwrap_err<+Drop<T>>(self: Result<T, E>) -> E;
fn is_ok(self: @Result<T, E>) -> bool;
fn is_err(self: @Result<T, E>) -> bool;
}
expect
和unwrap
方法类似,它们都试图从Result<T, E>
中提取T
类型的值,当它处于Ok
变体时。如果Result
是 Ok(x)
,两个方法都返回值 "x"。然而,这两个方法的关键区别在于当Result
是Err
变量时的行为。expect
方法允许你提供一个自定义的错误信息(作为felt252
值)在panic时使用,从而让你获取更多对panic相关的控制和上下文。另一方面,unwrap
方法用一个默认的错误信息进行panic,提供的关于panic原因的信息较少。
expect_err
和unwrap_err
的行为完全相反。如果Result
是Err(x)
,两个方法都返回值x
。然而,这两个方法的关键区别是在Result::Ok()
的情况下。expect_err
方法允许你提供一个自定义的错误信息(作为felt252
值),在panic时使用,从而让你获取更多对panic相关的控制和上下文。另一方面,unwrap_err
方法用一个默认的错误信息进行panic,提供的关于panic原因的信息较少。
A careful reader may have noticed the <+Drop<T>>
and <+Drop<E>>
in the first four methods signatures. This syntax represents generic type constraints in the Cairo language. These constraints indicate that the associated functions require an implementation of the Drop
trait for the generic types T
and E
, respectively.
最后,is_ok
和is_err
方法是ResultTrait
trait提供的实用函数,用于检查Result
枚举值的成员。
is_ok
获取一个Result<T, E>
值的快照,如果Result
是Ok
成员,则返回true
,意味着操作成功。如果Result
是Err
成员,则返回false
。
is_err
接收一个对Result<T, E>
值的引用,如果Result
是Err
成员,意味着操作遇到了错误,则返回true
。如果 Result
是 Ok
成员,则返回 false
。
当你想在不消耗结果值的情况下检查一个操作的成功或失败时,这些方法很有帮助,允许你执行额外的操作或根据枚举成员做出决定,而不用解开(unwrap)它。
你可以在这里找到 ResultTrait
的实现。
有例子总是更容易理解。
请看一下这个函数签名:
fn u128_overflowing_add(a: u128, b: u128) -> Result<u128, u128>;
它接收两个u128整数,a和b,并返回一个Result<u128, u128>
,如果加法没有溢出,Ok
成员存储加法的和,如果加法溢出,`Err’成员存储溢出的值。
现在,我们可以在其他地方使用这个函数。比如说:
fn u128_checked_add(a: u128, b: u128) -> Option<u128> {
match u128_overflowing_add(a, b) {
Result::Ok(r) => Option::Some(r),
Result::Err(r) => Option::None,
}
}
这里,它接受两个 u128 整数,a 和 b,返回一个 Option<u128>
。它使用 u128_overflowing_add
返回的 Result
来确定加法操作的成功或失败。匹配表达式检查 u128_overflowing_add
的 Result
。如果结果是 Ok(r)
,它返回 Option::Some(r)
,其中包含求和结果。如果结果是 Err(r)
,它返回 Option::None
,表示操作因为溢出而失败。如果发生溢出,该函数不会引发恐慌。
让我们再举一个例子,演示一下unwrap
的使用。
首先我们导入必要的模块:
use core::traits::Into;
use traits::TryInto;
use option::OptionTrait;
use result::ResultTrait;
use result::ResultTraitImpl;
在这个例子中,parse_u8
函数接收一个felt252
的整数,并尝试用try_into
方法将其转换为u8
的整数。如果成功,它返回Result::Ok(value)
,否则它返回Result::Err('Invalid integer')
。
fn parse_u8(s: felt252) -> Result<u8, felt252> {
match s.try_into() {
Option::Some(value) => Result::Ok(value),
Option::None => Result::Err('Invalid integer'),
}
}
我们的两个测试案例是:
fn parse_u8(s: felt252) -> Result<u8, felt252> {
match s.try_into() {
Option::Some(value) => Result::Ok(value),
Option::None => Result::Err('Invalid integer'),
}
}
#[cfg(test)]
mod tests {
use super::parse_u8;
#[test]
fn test_felt252_to_u8() {
let number: felt252 = 5_felt252;
// should not panic
let res = parse_u8(number).unwrap();
}
#[test]
#[should_panic]
fn test_felt252_to_u8_panic() {
let number: felt252 = 256_felt252;
// should panic
let res = parse_u8(number).unwrap();
}
}
第一个测试函数是测试从felt252
到u8
的有效转换,期望unwrap
方法不要panic。第二个测试函数试图转换一个超出u8
范围的值,期望unwrap
方法panic,错误信息是 'Invalid integer'。
我们也可以在这里使用 #[should_panic] 属性。
?
运算符?
我们要谈的最后一个操作符是?
操作符。?
运算符用于更成文和简明的错误处理。当你在 Result
或 Option
类型上使用?
运算符时,它将做以下事情:
- 如果值是
Result::Ok(x)
或Option::Some(x)
,它将直接返回内部值x
。 - 如果值是
Result::Err(e)
或Option::None
,它将通过立即从函数返回来传播错误或None
。
当你想隐式处理错误并让调用函数处理它们时,?
操作符很有用。
下面是一个例子。
fn do_something_with_parse_u8(input: felt252) -> Result<u8, felt252> {
let input_to_u8: u8 = parse_u8(input)?;
// DO SOMETHING
let res = input_to_u8 - 1;
Result::Ok(res)
}
do_something_with_parse_u8
函数接收一个felt252
值作为输入并调用parse_u8
。?
操作符用来传播错误,如果有的话,或者unwrap成功的值。
这里还有一个小的测试案例:
fn parse_u8(s: felt252) -> Result<u8, felt252> {
match s.try_into() {
Option::Some(value) => Result::Ok(value),
Option::None => Result::Err('Invalid integer'),
}
}
fn do_something_with_parse_u8(input: felt252) -> Result<u8, felt252> {
let input_to_u8: u8 = parse_u8(input)?;
// DO SOMETHING
let res = input_to_u8 - 1;
Result::Ok(res)
}
#[cfg(test)]
mod tests {
use super::do_something_with_parse_u8;
use debug::PrintTrait;
#[test]
fn test_function_2() {
let number: felt252 = 258_felt252;
match do_something_with_parse_u8(number) {
Result::Ok(value) => value.print(),
Result::Err(e) => e.print()
}
}
}
控制台将打印错误 “Invalid Integer”。
总结
我们看到,可恢复的错误可以在Cairo中使用结果枚举来处理,它有两个变体:Ok
和Err
。Result<T, E>
枚举是通用的,其类型T
和E
分别代表成功和错误值。ResultTrait
提供了处理Result<T, E>
的方法,例如解包值,检查结果是Ok
还是Err
,以及用自定义消息进行panic。
为了处理可恢复的错误,一个函数可以返回一个Result
类型,并使用模式匹配来处理操作的成功或失败。?
操作符可用于通过传播错误或解包成功的值来隐含地处理错误。这使得错误处理更加简洁明了,调用者负责管理由被调用函数引发的错误。