在其他传统语言的异常处理中,最常见的模式就是try / catch
结构,C/C++如此,Java也是如此。在try / catch
模式下,我们可以自由的选择想要处理的异常,甚至于可以直接忽略异常。但是在Rust中,异常却变成了一个编程人无法回避的事情。
Rust中的异常不是程序崩溃,其行为是明确定义的,只是它表示在程序中是不应该发生的事情而已。所有的异常都是安全的,不违反任何Rust中的安全规则。不管在哪个语言中,异常都是防止错误继续扩大的武器。
Result
类型
Rust中是没有异常的,函数的执行失败可以通过函数的返回值来表示。这一点与Golang中的函数返回多个值以附带表示函数的失败执行结果十分类似。但是与Golang中直接返回多个值不同,Rust 采用了一个专用的数据类型来表示函数的正常执行结果和异常执行结果,这个数据类型就是Result
。
一般的函数在执行结束后,要么会返回一个正常的执行结果,要么会返回一个异常,基本上不会出现既返回一个正常执行结果又返回一个异常的。所以Result
类型通过携带两个泛型类型来表示函数的两种执行结果,例如Result<i64, io::Error>
表示函数成功完成执行将会返回一个i64
类型的值,如果出现异常将会返回一个io::Error
类型的异常。
在函数中,可以通过返回Ok(value)
函数执行结果来从函数中返回一个成功的结果,而通过返回Err(error_value)
函数执行结果则可以从函数中返回一个错误的结果。
通过使用Result
类型,Rust强制要求编程人在调用这个可能返回错误结果的函数时,必须编写错误处理逻辑,而如果没有编写错误处理逻辑,那么Rust编译将发出警告。
捕获异常
在Rust中捕获异常实际上就是对函数返回的Result
类型的返回值进行处理。因为Rust没有try / catch
结构,所以对于Result
类型的处理是通过match
表达式来完成的。
例如对于函数fn get_statistics(scores: &Vec<i32>) -> Result<i32, io::Error>
来说,可以如同下例中一样来处理函数的返回值。
|
|
在match
表达式中,Ok()
和Err()
两个函数分别可以将Result
类型返回值中携带的内容解析出来赋予它们的参数。
但是在日常的程序中,到处都使用match
表达式捕获错误就有些过于啰嗦了,所以Result
类型还提供了一系列的常用方法来简化对于错误的处理。这些方法中比较常用的有以下这些。
result.is_ok()
和result.is_err()
,可以返回一个布尔值,分别用于判断Result
类型实例中携带的是成功结果还是失败的结果。result.ok()
,返回一个Option<T>
类型的值,如果Result
类型中携带的成功值,那么会通过Some(value)
返回其中的值,否则就会返回None
以表示丢弃错误值。result.err()
,与result.ok()
类似但是会返回Option<E>
类型的值,并且会丢弃成功值。result.unwrap_or(fallback)
,在Result
类型实例中携带成功值的时候直接返回成功值,否则将直接返回fallback
后备值。result.unwrap_or_else(fallback_fn)
,与result.unwrap_or()
类似,但是其后备值是通过闭包计算得到的,这个在使用的时候会比result.unwrap_or()
更加节省内存。result.unwrap()
,直接返回Result
类型实例中的成功值,但是如果Result
类型实例中携带的是错误值,那么就会直接产生异常。result.expect(message)
,直接返回Result
类型实例中的成功值,但是如果Result
类型实例中携带的是错误值,那么将会采用message
作为异常信息输出到控制台。reuslt.as_ref()
,将类型Result<T, E>
转换为Result<&T, &E>
,以提供成功值和错误值的引用。result.as_mut()
,将类型Result<T, E>
转换为Result<&mut T, &mut E>
。
result.is_ok()
和result.is_err()
以外,其他的方法都会用掉result
值,所以这也是result.as_ref()
和result.as_mut()
存在的意义。
异常冒泡
在大多数情况下,在异常发生的地方捕获和处理错误是不现实的。有相当一部分的错误更适合抛给函数或者方法的调用者去处理。在这种情况下,就用到了Rust提供的沿调用栈向上传播错误的语法。
要传播错误是非常简单的,只需要在任何会产生Result
类型返回值的表达式后面添加?
操作符就可以了。?
操作符可以根据Result
类型结果中携带的值不同,决定是提取其中的成功值还是将错误值向上抛出。
例如之前定义的函数示例,如果要向上抛出错误值只需要这样做。
|
|
自定义错误类型
Rust这的错误实际上就是一个类型,可以通过自定义一个结构体来定义。以下示例就定义了一个十分简单的错误类型。
|
|
这个自定义的错误类型在函数中可以通过以下方式返回使用。
|
|
但是仅仅这样定义异常还不能够让这个错误的行为更加接近标准库中的标准错误类型。如果想要达到接近标准错误类型的目的,还需要让其实现一些指定的特征(Trait)。
|
|
实现了这两个特征以后,自定义的新错误类型的行为就更加接近标准库的中的标准错误类型了。
Result<T>
类型,其实是Result<T, E>
类型的别名。通过定义别名类型,可以省略掉那些需要反复书写的错误类型声明。这个错误类型的别名中所可以使用的错误类型可以去到类型别名定义的位置查看。
错误类型的包装
其实对于错误类型的包装,主要是使用在函数或者方法中可能能出现多种类型的错误的情况下。在函数中返回的错误类型与函数签名中定义的错误类型不一致的时候,Rust会首先尝试进行错误类型转换,但是这种转换并不能保证一定成功。如果Rust所尝试的错误类型转换没有成功,那么我们就又收获了一条类型错误。
要解决这个问题,第一个可以被想到的方法就是提供一条错误类型的转换路径。其实多种类型的错误在统一个函数中需要处理的时候,通常会将这些错误用一个枚举类型封装起来,并且实现Display
、Debug
、Error
和From<T>
这几个特征。这条路径手写起来比较枯燥繁琐,一般可以借助于thiserror
包(用于库)或者anyhow
包(用于bin程序)的辅助来提速实现。
|
|
其中#[error()]
可以用于为错误指定一条描述信息,#[from]
可以为错误实现From
特征。这个错误类型在函数中只需要使用?
操作符抛出错误即可使用。
如果不借助thiserror
包的辅助,那么所需要编写的内容大概有以下这些。
|
|
另一种思路是利用Rust的特性,所有的标准库错误类型都可以被转换为Box<std::error::Error>
类型,所以就会产生一个十分简单的错误类型包装方案。
|
|
这样一来,程序在运行的时候,就会自动将任意错误类型转换为CustomError
类型。除了可以利用?
操作符进行自动类型转换以外,还可以为自定义错误类型实现From
特征,来手动完成错误类型的转换。例如
|
|
使用Anyhow简化错误处理
前文提到过Anyhow这个库,这个库的主要功能是在可编译为可执行程序的应用中使用的。在使用标准库中提供的Result
类型以后,就会发现在一个函数或者应用的一个功能中,经常需要抛出各种不同的错误。虽然这些错误都实现了std::error::Error
特征,但是在捕获和处理它们的时候依旧是需要严格的声明它们的类型的,或者需要书写具体错误的泛型约束,所以在实际使用中就会显得有些繁琐了。
Anyhow为这种情况提供了一个更加通用和智能的Result
类型,所有返回标准库中Result<T, Error>
的位置,都可以使用Result<T, anyhow::Error>
或者anyhow::Result<T>
来代替,而且Anyhow还可以在Result
类型中记录上下文信息。
以下是一个使用Anyhow库的示例,从其中可以看出,使用Anyhow来简化标准库中的错误处理并不复杂。
|
|
除了Result
类型和Context
特征以外,Anyhow还提供了几个比较有用的宏。
anyhow!
可以用来利用字符串直接产生一个anyhow::Error
。bail!
相当于return Err(anyhow!($args...));
,可以用来快速返回一个错误。ensure!
相当与if !expr { return Err(anyhow!($args...)); }
,也就是当指定条件不满足的时候快速返回一个错误。
bail!
和ensure!
两个宏的时候,需要确保函数返回的值类型为Result<T, anyhow::Error>
或者anyhow::Result<T>
类型。