可恢复的错误与 Result


大多数错误并没有严重到需要程序完全停止的程度。有时,当一个函数失败时,它的原因是你可以很容易地解释和应对的。例如,如果你试图将两个大的整数相加,而操作溢出,因为总和超过了最大的可表示值,你可能想返回一个错误或一个包装好的结果,而不是引起未定义行为或终止程序。

Result枚举

回顾第8章中的“通用数据类型” 一节中,Result 枚举被定义为具有两个变体,即 OkErr,如下所示:

enum Result<T, E> {
    Ok: T,
    Err: E,
}

Result<T, E>枚举有两个泛型类型,TE,以及两个成员:Ok,存放T类型的值,Err,存放E类型的值。这个定义使得我们可以在任何地方使用Result枚举,该操作可能成功(返回T类型的值)或失败(返回E类型的值)。

ResultTrait

ResultTraittrait提供了处理Result<T, E>枚举的方法,例如解包值,检查ResultOk还是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;
}

expectunwrap方法类似,它们都试图从Result<T, E>中提取T类型的值,当它处于Ok变体时。如果ResultOk(x),两个方法都返回值 "x"。然而,这两个方法的关键区别在于当ResultErr变量时的行为。expect方法允许你提供一个自定义的错误信息(作为felt252值)在panic时使用,从而让你获取更多对panic相关的控制和上下文。另一方面,unwrap方法用一个默认的错误信息进行panic,提供的关于panic原因的信息较少。

expect_errunwrap_err的行为完全相反。如果ResultErr(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_okis_err方法是ResultTraittrait提供的实用函数,用于检查Result枚举值的成员。

is_ok获取一个Result<T, E>值的快照,如果ResultOk成员,则返回true,意味着操作成功。如果ResultErr成员,则返回false

is_err接收一个对Result<T, E>值的引用,如果ResultErr成员,意味着操作遇到了错误,则返回true。如果 ResultOk成员,则返回 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_addResult。如果结果是 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'),
    }
}

示例10-1:使用Result 类型

我们的两个测试案例是:

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

第一个测试函数是测试从felt252u8的有效转换,期望unwrap方法不要panic。第二个测试函数试图转换一个超出u8范围的值,期望unwrap方法panic,错误信息是 'Invalid integer'。

我们也可以在这里使用 #[should_panic] 属性。

?运算符?

我们要谈的最后一个操作符是?操作符。?运算符用于更成文和简明的错误处理。当你在 ResultOption类型上使用?运算符时,它将做以下事情:

  • 如果值是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)
}

示例 10-1: 使用 ? 操作符

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中使用结果枚举来处理,它有两个变体:OkErrResult<T, E>枚举是通用的,其类型TE分别代表成功和错误值。ResultTrait提供了处理Result<T, E>的方法,例如解包值,检查结果是Ok还是Err,以及用自定义消息进行panic。

为了处理可恢复的错误,一个函数可以返回一个Result类型,并使用模式匹配来处理操作的成功或失败。?操作符可用于通过传播错误或解包成功的值来隐含地处理错误。这使得错误处理更加简洁明了,调用者负责管理由被调用函数引发的错误。

Last change: 2023-12-09, commit: acd03a1