Skip to content

33.1 基本错误处理

Rust 的错误处理机制,主要还是基于返回值的方案。不过因为拥有代数类型系统这套机制,所以它比 C 语言那种原始的错误码方案表达能力更强一点。Rust 用于错误处理的最基本的类型就是我们常见的Option<T>类型。比如,内置字符串类型有一个 find 方法,查找一个子串:

rust
impl str {
    pub fn find<'a, P: Pattern<'a>>(&'a self, pat: P) -> Option<usize> {}
}

这个方法当然是可能失败的,它有可能找不到。为了表达“成功返回了一个值”以及“没有返回值”这两种情况,Option<usize>就是一个非常合理的选择。而在传统的 C 语言里面,由于缺乏代数类型系统,往往会选择使用返回类型中的某些特殊值来表示非正常的情况。比如,C 标准库中的子串查找函数的签名是这样的:

rust
char *strstr( const char* str, const char* substr ) {}

返回的就是 char * 指针,使用空指针代表没找到的情况。

对于这种简单的错误处理,这么凑合一下问题也不大。如果错误信息更复杂一些,比如既需要用错误码表示错误的原因,又需要返回正常的返回值,就麻烦一些了。一般 C 语言的做法是,用返回值代表错误码,把真正需要返回的内容使用指针通过参数传递出去。比如 C99 里面打开文件的函数是这样设计的:

rust
FILE *fopen( const char *restrict filename, const char *restrict mode );

到了 C11 又新增了一个支持多个错误码的打开文件的函数:

rust
errno_t fopen_s(FILE *restrict *restrict streamptr,
                const char *restrict filename,
                const char *restrict mode);

这种方式明显是牺牲了可读性和使用方便性的。

在 Rust 里面就不用这么麻烦了。我们可以使用 Result<T,E> 类型来处理这种情况,干净利落:

rust
impl File {
    pub fn open<P: AsRef<Path>>(path: P) -> io::Result<File> {}
}

上面这个例子使用了std::io::Result类型,而不是std::result::Result类型。但是实际上它们是一回事,因为在 io 模块中有个类型别名的定义:

rust
pub type Result<T> = result::Result<T, Error>;

这就是说,std::io::Result<T> 等于 std::result::Result<T,std::io::Error>。只是把泛型中的 E 参数定成了 std::io::Error 这个具体类型而已。

代数类型系统是错误处理的利器。我们再看一个例子。标准库中有一个 FromStr trait:

rust
pub trait FromStr: Sized {
    type Err;
    fn from_str(s: &str) -> Result<Self, Self::Err>;
}

如果我们想从一个字符串构造出一个类型的实例,就可以对这个类型实现这个 trait。当然这个转换是有可能出错的,所以 from_str 方法一定要返回一个 Result 类型。比如针对 bool 类型实现的这个 trait:

rust
impl FromStr for bool {
    type Err = ParseBoolError;
    #[inline]
    fn from_str(s: &str) -> Result<bool, ParseBoolError> {
        match s {
            "true"  => Ok(true),
            "false" => Ok(false),
            _       => Err(ParseBoolError { _priv: () }),
        }
    }
}

正常情况是 bool 类型,异常情况是 ParseBoolError 类型。这个 ParseBoolError 不需要携带其他额外信息,所以它是一个空结构体就够了。

我们再看看 FromStr 针对 f32 的实现。可以看到,从字符串解析浮点数可能出现的错误种类更多,所以错误类型被设计成了一个 enum:

rust
enum FloatErrorKind {
    Empty,
    Invalid,
}

最后我们再看看 FromStr 针对 String 的实现。因为从 &str 到 String 的这个转换一定是可以成功的,不存在失败的可能,所以这种情况下错误类型被设计成了空的 enum:

rust
pub enum ParseError {}

前面我们已经说过了,空的 enum 就是 bottom type,等同于发散类型!。所以这个错误实际上没有任何额外性能开销,证明如下:

rust
use std::str::FromStr;
use std::string::ParseError;
use std::mem::{size_of, size_of_val};

fn main() {
    let r : Result<String, ParseError> = FromStr::from_str("hello");
    println!("Size of String: {}", size_of::<String>());
    println!("Size of `r`: {}", size_of_val(&r));
}

这个返回类型 Result<String,ParseError> 实际上和 String 是同构的。

所以说,Rust 的这套错误处理机制既具备良好的抽象性,也具备无额外性能损失的优点。

Released under the MIT License