rust错误处理有2中方式:panic和Result,普通的错误用Result处理。
当程序遇到无法处理的错误的时候会触发panic,比如:数组越界访问,除法除数为0,对当Option类型值为None的时候调用.unwrap()方法,断言失败。
也可以用宏panic!()手动触发恐慌。panic!()宏支持参数,像println!()那样,打印错误信息。
rust遇到恐慌时可以有2中处理方式,一种是栈回滚,一种是终止进程。默认是回滚。
假如你设置了RUST\_BACKTRACE环境变量,那么恐慌的时候rust会打印完整的栈信息。
栈回滚的时候,任何临时的值,局部变量和参数都会被丢弃,按它们被创建的相反的顺序。丢弃意味着释放内存空间和相关的io关闭等操作,用户自定义的drop方法也会被调用。当前函数调用的资源清理完之后再按照相同的方法清理上层调用,直到栈顶函数的清理,最后线程退出,如果恐慌的线程是主线程,那么进程会退出,退出码非零。
也许恐慌是一个引起人误导的名字,恐慌不是崩溃,它不是未定义的行为,它更像java里的运行时异常或者C++里的std::logic\_error。它的行为是定义好的。
panic是安全的,它并没有违反任何rust的安全规则。它绝对不会导致垂悬指针或者半初始化的值在内存。遇到panic的时候如果继续运行程序会导致不安全,所以rust进行栈回滚。恐慌是线程级别的,一个线程恐慌了,但是进程里面其他的线程可以继续正常运行。也有方法可以捕获住恐慌,允许线程存活继续运行,标准库提供的方法std::panic::catch\_unwind()就用于捕获恐慌。这是测试经常用的机制,如果断言失败,把它捕获继续运行其他的测试。栈回滚如果遇到非rust的代码是未定义的行为。你可以用catch\_unwind来处理恐慌,但是有个限制,这个方法仅对栈回滚的恐慌有用,不是所有的恐慌都会进行栈回滚。
栈回滚是默认的恐慌处理方式,但是有2中情况rust不会进行栈回滚。
一种情况,当正在进行回滚的时候,执行.drop()方法再次触发了恐慌,这被认为是严重错误,rust会终止回滚并终止进程。
另一种情况,你在编译程序的时候用了-C panic=abort参数编译,在首次恐慌的时候就会直接终止进程。用这个参数rust不需要记住怎么进行栈回滚,因此编译后的代码尺寸会更小。
Result<T, E>提供了各种方法应对特别的场景。
result.is_ok()和result.is_err()返回bool值告诉我们一个result是成功还是错误。这两个方法不会消费掉result。
result.ok()返回Option<T>,如果是成功的就返回Some<T>,如果是错误,返回None,错误信息会被丢弃。
result.err()返回Option<E>,如果是错误,就返回Some<E>,否则返回None。
result.unwrap_or(fallback),fallback是替补,类型必须和T一样,返回T类型的值,如果是成功的就返回携带的值,如果是错误,就返回fallback,丢弃掉错误信息。这个方法直接返回的是T类型的值,不像result.ok()那样返回Option<T>,会方便些。
result.unwrap_or_else(fallback_fn)相同, 但是传递的是一个返回T类型的函数或者闭包,这适合在提供替补值比较耗时的场景,替补函数只有在result是错误的时候才会执行。
result.unwrap()如果result是成功就返回成功的值,类型是T;然而假如result是错误,那么该方法会触发恐慌。
result.expect(message)和.unwrap()方法相同,但是允许你提供错误信息在恐慌的时候打印在控制台。
result.as_ref()转换Result<T, E>成Result<&T, &E>。
result.as_mut()和.as_ref()一样,但是生成的是可变引用,返回类型为Result<&mut T, &mut E>。
.as_ref()和.as_mut()这两个方法比较有用,其它的方法除了.is_ok()和.is_err()都会消费掉result,有时候需要访问result内部的数据但是不能销毁它.as_ref()和.as_mut()帮我们做到这一点。
模块经常定义一个Result别名,提供固定的错误类型,以简化代码,防止在用到Result的每一个地方都要写一个Error,比如如下代码:
pub type Result<T> = result::Result<T, Error>;
标准库提供了各种错误类型,比如std::io::Error,std::fmt::Error,std::str::Utf8Error等等,他们都实现了一个公共接口std::error::Error,表示它们都具有以下特征:
它们都可以用宏println!()打印。打印一个错误用{}格式只会显示一点儿简单的信息,你可以用{:?}格式获得错误的Debug信息,这样可读性差了点儿,但是携带了更多信息。
err.description()返回一个&str类型的错误信息。
err.cause()返回一个Option<&Error>更底层的错误,一个导致当前err的错误,没有就返回None。根cause的.cause()方法就会返回None。
由于标准库只包含低级别的特征,所以标准库的错误的.cause()方法经常返回None。
打印一个错误并不会打印它的cause,要想打印更多的错误栈信息,需要自定义打印函数。
标准库的错误类型不包含栈信息,但是error-chain包让你定义自定义错误类型和抓取栈信息更简单,它用了backtrace包来捕获栈信息。
rust有一个?操作符,你可以添加?到Result类型值的后面。?的行为依赖于result的结果是成功还是错误:
如果成功,它会提取里面的值,像.unwrap()方法那样;如果失败它会执行return立即返回当前函数,并把错误返回,返回的错误也是Result类型,为了确保?正常工作,在使用?的函数必须返回Result类型。
在旧代码里,你可能看到try!()宏,它展开一个match表达式,做了类似?的工作。
error-chain包可以帮助你用几行代码定义好的错误类型。
所有的标准库错误类型可以转换成Box<std::error::Error>,表示任意的错误,因此一个简单的方法处理多错误类型可以定义如下别名:
type GenError = Box<std::error::Error>;
type GenResult<T> = Result<T, GenError>;
?操作符会在需要的时候自动转换各种错误的类型成GenError。
?操作符的自动转换是用了标准库提供的方法,你也可以用GenError::from()函数手动转换,它可以转换任意错误成GenError类型。
GenError的缺点是它不再精确地表示具体的错误类型了,调用者需要准备好处理任何类型的错误。如果你正在调用返回GenResult的函数,你想要处理一种特殊的错误类型,让其它的类型继续冒泡,可以用error.downcast_ref::<ErrorType>()方法,它返回Option<ErrorType>,如果错误类型恰好是ErrorType类型,那么返回该错误的引用??,否则返回None。
许多语言有内建的语法处理错误类型的匹配,但是它们经常很少被用,rust用简单的方法来处理这种情况。
如果错误不可能发生,那么获取结果最好的方法是.unwrap(),但是如果错误会发生,那么将导致恐慌。
未处理的Result编译器会报警告,如果不需要处理Result,可以用let _ = ...通配符来去掉警告。
main()函数里面不能用?表达式,因为它的返回值类型不是Result,在main()函数里处理Result最简单的方法就是调用.expect()方法。
用RUST_BACKTRACE=1环境变量来打印详细错误信息不是个好方法,你应该自定义函数来打印错误。
如果你希望你自己的错误类型能像标准错误类型那样工作,你的库的使用者希望是这样,你需要让自己的错误类型实现std::error::Error和fmt::Display两个特征。
rust需要程序员做各种决定,并且对每一个可能出错的地方进行处理,这是好的设计,因为如果你忽略了错误处理,程序就很容易出错。
最常见的处理方法是用?表达式来让错误冒泡,错误处理不会像C或者Go那样杂乱,它仍然是可见的,你可以用眼睛扫一下代码,就知道哪些地方有错误冒泡。
由于可能的错误是函数返回值的一部分,通过函数定义就能看出哪些函数可能出错,哪些不能出错。如果你修改一个函数为可以失败,你需要修改返回值的类型,而编译器会告诉你,哪些地方被使用者调用也需要修改。
Rust会检查Result是否被处理,所以你不可能灾难性的让错误通过。
由于Result跟其他类型一样是个数据结构,它可以很容易的存储成功和错误的结果到同一个集合里面,很容易用集合来表示部分成功。
你付出的代价是比其他语言思考更多错误处理的细节,rust让你对错误的处理更紧一点,对系统级编程来说,这是值得的。
