05 - 错误处理
- Rust 有两种不同的错误处理机制:
- 诧异(
Panic
) Result
- 诧异(
- 普通的错误使用
Result
处理。- 由程序外部的事情导入:输入错误、网络中断,或者权限问题
- 即时写的再完美的程序,也会不时地碰到这样的错误。
- 诧异用于处理 “永远不该发生的错误”,比如程序本身的 Bug。
5.1-Panic
- 以下常见的程序 Bug,都会产生诧异:
- 越界访问数组
- 整数被零除
- 在值为
None
的Option
上调用.unwrap()
- 断言失败
- Rust 有一个同名的宏
Panic!()
,用于在代码中主动触发诧异。接收可选的println!()
风格的参数,用于构建错误消息。 - 针对诧异的规避措施:
- 在诧异发生时展开(unwind)栈,是默认选项;
- 中止程序。
5.1.1 - 展开栈
- 当前函数调用被清理后,控制流交还至调用者,再以同样的方式清除其变量和参数。接下来是那个函数的调用者,总之沿栈逐层清理。
- 当前函数石油的任何临时值、局部变量或参数都会按照它们创建的顺序被反向清除。
- 最后,线程退出。
- 如果发生诧异的线程是主线程,那么整个程序都会退出,退出码为非 0.
- 诧异是安全的,其行为是明确定义的。如 Java 语言中的
RuntimeException
或 C++ 语言中的std::logic_error
。 - 诧异是线程级别的:一个线程诧异时,其他线程可以正常运行自己的业务逻辑。
- 标准库函数
std::panic::catch_unwind
,可以捕获栈展开,允许诧异线程存活并继续进行。- Rust 的测试套件在断言失败时,使用了这个机制来恢复线程执行。
- 可以使用线程和
catch_unwind()
来处理诧异。 - 这种机制只能捕获展开栈的诧异,而并非所有诧异都会展开栈(另一些会中止进程)。
5.1.2 - 中止进程
在以下两种情况,程序发生 Bug 后会中止进程:
-
在 Rust 展开第一个函数后的清理期间,
.drop()
方法触发了第二个诧异:
- 这种诧异会被认为是间接致使的,Rust 会停止展开栈并中止整个进程。
-
Rust 的诧异行为是自定义的情况:
- 编译时加上
-C panic=abort
,那么编译后程序中的第一个诧异就会立即中止进程。 - 这个参数设置,使得 Rust 编译器无须知道如何展开栈,因此可以减少编译后代码的大小。
- 编译时加上
5.2-Result
-
Rust 没有异常,但是相反,当函数执行失败时,可以通过一个返回的
Result
类型来表示。fn get_weather(location: LatLng) -> Result<WeatherReport, io::Error>
- 上述函数执行成功,会返回
Ok(weather)
,此时weather
的值是一个新的WeatherReport
值。 - 执行失败,会返回
Err(error_value)
,此时error_value
就是一个解释除了什么错误的io::Error
值。
- 上述函数执行成功,会返回
5.2.1 - 捕获错误
-
处理
Result
的一种方式,是使用match
表达式。// 相当于Python中的try机制 match get_weather(hometown) { ok(report) => { display_weather(hometown, &report); } Err(err) => { println!("error querying the weather: {}", err); schedule_weather_retry(); } }
-
处理
Result
的另一种方式,是使用Result<T, E>
提供的几种方法,每种方法都有一个形如上述的match
表达式。如下所示常用的方法有:-
result.is_ok()
和result.is_err()
返回bool
值:告知result
是成功的结果还是错误的结果。 -
result.ok()
返回Option<T>
类型的成功值:成功返回Some(success_value)
;失败返回None
,并丢弃错误值。 -
result.err()
返回Option<E>
类型的错误值。 -
result.unwrap_or(fallback)
返回T
类型的成功值。如果失败返回一个后备值fallback
,并丢弃错误值。// 设定一个后备值,用来预测某地天气 const THE_UAL: WeatherReport = WeatherReport::Sunny(72); // 如果预测成功,返回实时的天气情况 // 如果预测失败,则以常规的值为后备 let report = get_weather(los_angeles).unwrap_or(THE_UAL); display_weather(los_angeles, &report);
-
result.unwrap_or_else(fallback_fn)
返回T
类型的成功值。但是传入的参数是一个函数或闭包。只有在返回错误结果时,才会调用fallback_fn
。let report = get_weather(hometown) .unwrap_or_else(|_err| vague_prediction(hometown));
-
result.unwrap()
会返回T
类型的成功值。如果失败,会导致诧异。 -
result.expect(message)
与result.unwrap()
相同,不过需要提供诧异时打印到控制台的消息message
。
-
-
借用
Result
中值的引用的方法,Result
的原数据结果不会损坏:result.as_ref()
将Result<T, E>
转换为Result<&T, &E>
,即借用现有result
中成功或错误的引用。result.as_mut()
,借用的是可修改引用。返回的类型为Result<&mut T, &mut E>
。
5.2.2-Result
类型别名
-
省略
Result
的错误类型,即为Result
类型别名:fn remove_file(path: &Path) -> Reust<() >
- 避免模块中的每个函数将一个错误类型重复写很多遍。
pub type Result<T> = result::Result<T, Error>;
- 上述代码定义了一个公有类型
std::io::Result<T>
,以硬编码的std::io::Error
作为错误类型的Result<T, E>
的别名。实际使用中,如果use std::io
,那么 Rust 就会认为io::Reust<String>
是Result<String, io::Error>
的简写
5.2.3 - 打印错误
-
将错误转存到终端,然后继续执行:
println!("error querying the weather: {}", err);
-
std:io::Error
、std::fmt::Error
、std::str::Utf8Error
等错误类型有相同的公共接口:std::error::Error
特型,它们都支持以下特点:- 都可以使用
println!()
来打印。打印错误时以{}
作为格式描述符,可以显示简略的错误消息;也可以使用{:?}
为格式描述符,这样可以打印 debug 版的错误消息。 - 支持
err.description()
方法,返回&str
类型的错误消息。 - 支持
err.cause()
方法,返回一个Option<&Error>
类型,这是触发err
的底层错误。即错误的真正原因。
- 都可以使用
-
打印错误消息不一定会打印出错误的原因。如果需要打印所有可用信息,可使用如下函数:
use std::error::Error; use std::io::{Write, stderr}; /// 把错误消息转存到stderr /// /// 如果在构建当前错误消息,或写入stderr时,发生了另一个错误,则忽略该错误。 fn print_error(mut err: &Error) { let _ = writeln!(stderr(), "error: {}", err); while let Some(cause) = err.cause() { let _ = writeln!(stderr(), "caused by: {}", cause); err = cause; } }
-
标准库的错误类型不包含栈追踪信息,但是使用
error-chain
包可以方便地定义自己的错误类型,以支持在创建时获取栈追踪信息。这个包使用了backtrace
捕获栈信息。
5.2.4 - 传播错误
- 含义:为了避免代码冗余,可以不立即捕获和处理错误,让错误可以沿调用栈向上传播。
?
操作符try!()
宏
?
操作符:可以在任何产生Result
的表达式后面添加?
,如:
let weather = get_weather(hometown)?;
-
如果返回了成功结果,那么
?
操作符会打开Result
并取出其中的成功值。 -
如果返回了错误结果,那么
?
操作符会立即从闭合函数中返回,将错误结果沿调用链向上传播。 -
只能对返回值为
Result
的函数使用?
。 -
上述代码等价于:
let weather = match get_weather(hometown) { ok(success_value) => success_value; Err(err) => return Err(err) };
-
try!()
宏:扩展为一个类似上述match
的表达式。let weather = try!(get_weather(hometown));
-
?
操作符的用法举例:use std::fs; use std::io; use std::path::Path; fn move_all(src: &Path, dst: &Path) -> io::Result<()> { for entry_result in src.read_dir()? { // 打开dir可能失败 let entry = entry_result?; // 读取dir可能失败 let dst_file = dst.join(entry.file_name()); fs::rename(entry.path(), dst_file)?; // 重命名可能失败 } ok(()) // 如果执行成功,则返回成功值 }
5.2.5 - 处理多种错误类型
-
从文件中读取一行内容,并解析为整数时,会产生两种不同的潜在错误类型:
use std::io::{self, BufRead}; /// 从文本文件中读取整数 /// 这个文件中的每一行应该都有一个数值 fn read_numbers(file: &mut BufRead) -> Result<Vec<i64>, io::Error> { let mut numbers = vec![]; for line_result in file.lines() { let line = line_result?; // 读取行的内容可能失败 numbers.push(line.parse?); // 解析整数有可能失败 } Ok(numbers) }
-
line_result
的错误类型是Result<String, std::io::Error>
; -
line.parse()
的错误类型是Result<i64, std::num::ParseIntError>
;在编译时,Rust 会常识把ParseIntError
转换为io::Error
,但是这种转换不存在,就会得到如下的类型错误:the trait 'std::convert::From<std::num::ParseIntError>' is not implemented for 'std::io::Error'
-
-
处理上述错误的方法:
-
采用
error-chain
包; -
采用内置特性:所有标准库的错误类型都可以转换为
Box<std::error::Error>
类型,因此可以定义如下类型别名:type GenError = Box<std::error::Error>; type GenResult<T> = Result<T, GenError>; // 然后,可以把read_numbers()的返回类型改为GenResult<Veci64> // ?操作符,会根据需求,自动将任意错误类型转换为GenError。
-
把任意错误转换为
GenError
类型,也可以调用GenError::from()
方法。let io_error = io::Error::new( // 创建一个自定义的io::Error io::ErrorKind::Other, "timed out" ); return Err(GenError::from(io_error)); // 手工转换为GenError
-
使用
GenError
的缺点:返回类型不再精确地传达调用者可以预测的错误类型。如果调用的函数返回GenResult
,但只想处理一种特定的错误,让其他所有错误传播出去,那么可以使用泛型方法error.downcast_ref::<ErrorType>()
。如果恰好是想要的那个错误类型,那么该方法会借用这个错误的引用:loop { match compile_project() { Ok(()) => return Ok(()), Err(err) => { if let Some(mse) = err.downcast_ref::<MissingSemicolonError>() { insert_semicolon_in_source_code(mse.file(), mse.line())?; continue; } return Err(err); } } }
-
5.2.6 - 处理不会发生的错误
-
把数字字符串转换成实际的数值:
let num = str.parse::<u64>(); let num = str.parse::<u64>().unwrap(); // 处理当字符串不是数字时的编写错误,用诧异输出 // 但是如果数字过长,而超过了u64,那么此时会产生一个bug
-
如果错误代表的是一个非常严格或奇怪的条件,希望在出错时差异,那么可以采用如下的
.elapsed()
方法:fn print_file_age(filename: &Path, last_modified: SystemTime) { let age = last_modified.elapsed().expect("System clock drift"); ... } // .elapsed()方法,只有当系统时间早于文件创建时间时才会失败 // 此时会出发诧异,而不是处理错误或将错误传播给调用者。
5.2.7 - 忽略错误
-
let _ = ...
:用来禁止某些错误- 把 stderr 通过管道发送到另一个进程,但是该进程被杀死了,会触发难以避免的错误。可以通过以下方法忽略这个错误:
let _ = writeln!(stderr(), "error: {}", err);
5.2.8 - 在 main 中处理错误
-
main()
函数不能使用?
操作符 -
.expect()
方法:在main()
函数中处理错误的简单方式fn main() { calculate_tides().expect("error"); } // 主线程中的诧异会打印错误消息,然后以一个非零退出码退出。 // 在较小的程序中比较推荐使用。
-
优化上述代码:采用
if let
表达式,只在调用calculate_tides()
返回错误时打印错误消息。use std::error::Error; use std::io::{Write, stderr}; /// 把错误消息转存到stderr /// /// 如果在构建当前错误消息,或写入stderr时,发生了另一个错误,则忽略该错误。 fn print_error(mut err: &Error) { let _ = writeln!(stderr(), "error: {}", err); while let Some(cause) = err.cause() { let _ = writeln!(stderr(), "caused by: {}", cause); err = cause; } } fn main() { if let Err(err) = calculate_tides() { print_error(&err); std::process::exit(1); } }
5.2.9 - 声明自定义错误类型
use std;
use std::fmt;
// 编写一个JSON解释器,并让它有自己的错误类型
// json/src/error.rs,可以通过json::error::JsonError调用
#[derive(Debug, Clone)]
pub struct JsonError {
pub message: String,
pub line: usize,
pub column: usize,
}
/// 方法一:简单创建
return Err(JsonError {
message: "expected ']' at end of array".to_string(),
line: current_line,
column: current_column
});
/// 方法二:接近标准错误类型
// 错误可以打印出来
impl fmt::Display for JsonError {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
write!(f, "{} ({}:{})", self.message, self.line, self, self.column)
}
}
// 错误实现std::error::Error特型
impl std::error::Error for JsonError {
fn description(&self) -> &str {
&self.message
}
}
详见《Rust 程序设计》(吉姆 - 布兰迪、贾森 - 奥伦多夫著,李松峰译)第七章
链接地址