我知道只要活得足够久, 这种事就一定会发生. ---- 萧伯纳论死亡
本章介绍两类错误:
- panic: 永远不应该发生的错误
- Result: 普通错误, 通常表示程序外部的错 (IO, 网络中断, 权限问题)
7.1 panic
常见的panic问题:
- 数组越界
- 除零
Result
值为Err
上调用了.expect()
assert!
断言失败panic!
宏调用
Rust的目标是: 不要触发panic
Rust如何处理panic:
- 展开调用栈 (默认行为)
- 终止进程
7.1.1 处理panic: 展开调用栈
触发panic的函数
fn pirate_share(total: u64, crew_size: usize) -> u64 {
let half = total / 2;
half / crew_size as u64 // 存在除零panic风险
}
fn main() {
let money = pirate_share(13333, 0); // 传入的crew_size是0, 触发panic
println!("money = {}", money);
}
编译并运行, 处理panic的过程如下:
- 如果设置
RUST_BACKTRACE=1
Rust就会转储当前调用栈 - 展开调用栈(类似C++):
- 当前函数的任何临时值, 局部变量, 参数都会被回收 (汇编层面:
leave
栈回收) - 打开的文件都会被关闭
- 调用用户定义的drop方法
- 清理当前函数栈后,
ret
回到调用者中, 用相同的方式回收
- 当前函数的任何临时值, 局部变量, 参数都会被回收 (汇编层面:
- 线程推出, 如果panic线程是主线程, 则进程退出
Compiling example1-test v0.1.0 (/Users/lamecrow/TRY/code/rust/rustpg/chapter6/example1-test)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.12s
Running `target/debug/example1-test`
thread 'main' panicked at src/main.rs:3:5:
attempt to divide by zero
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
- 根据上方的过程描述, panic更类似于C++中的异常, 且panic的处理是安全的, 不会留下任何悬空指针或者未初始化值.
- 这种安全属性在多线程中有体现, 即某个线程panic后, 其他线程仍然能够安全运行
- panic也可以捕获并让程序继续运行, 在后续章节学习.
7.1.2 处理panic: 中止
调用栈展开是默认行为, 但是Rust在两种情况下直接中止:
- 二次panic: 处理第一个panic时,
.drop
方法触发了第二个panic - 主动中止: 自定义panic行为,
-C panic=abort
可以立即中止
7.1.3 panic与异常的区别
panic与异常区别:
- panic与Result分离
- 传统异常: 不区分错误是否可控, 传递均通过
try-catch
- Rust: 可控错误传递通过
Result
传递处理, 不可控错误通过panic
处理
- 传统异常: 不区分错误是否可控, 传递均通过
7.2 Result
Result与异常的区别:
- Result: 显示处理, 通过返回值来确定是否发生了异常
- 通过Result对返回值封装, 实现强制要求处理错误
- 异常: 隐式处理, 执行流不如Result更加符合直觉
fn get_weather(location: i32) -> Result<WeatherReport, io::Error> {
...
}
解析:
Result<WeatherReport, io::Error>
- 定义成功类型: Ok(weather: WeatherReport)
- 定义错误类型: Err(error_value: io::Error)
- 同时要求编写代码时必须编写错误处理代码(强制), 才能获取实际返回值
- 可以自定义类
Result
类型
7.2.1 捕获错误
处理错误的底层方法: match
针对直接处理错误, 而不是传递错误的情况.
// 确保数字大于0, 否则触发错误
fn positive(num: i32) -> Result<i32, i32> {
if num < 0 {
Err(1)
} else {
Ok(num)
}
}
fn main() {
let a = -1;
match positive(a) {
Ok(num) => {
println!("positive number: {}", num);
}
Err(err) => {
println!("Error number is {}", err);
}
}
}
简洁处理 (match封装)
- 返回bool值
result.is_ok()
result.is_err()
- 转化为
Option<T>
result.ok()
丢弃错误值, 并转换为Option<T>
,Ok(value)
返回Some(value)
,Err(err)
返回None
result.err()
丢弃正确值, 并转换为Option<T>
,Err(err)
返回Some(err)
,Ok(value)
返回None
- 回退值 (默认值)
result.unwrap_or(fallback)
,Ok(value)
返回value
,Err(err)
返回fallback
(与value同类型)
- 错误调用函数或者闭包: 针对不需要回退值的情况
result.unwrap_or_else(fallback_fn)
- 解包
result.unwrap()
,Ok(value)
返回value
,Err(err)
触发panic
- 期待
result.expect(message)
,Ok(value)
返回value
,Err(err)
触发panic且打印message
- 处理Result引用
result.as_ref()
将Result<T, E>
转换为Result<&T, &E>
result.as_mut()
将Result<T, E>
转换为Result<&mut T, &mut E>
fn get_number(valid: bool) -> Result<i32, String> {
if valid {
Ok(42) // 返回成功值 42
} else {
Err("出错了".to_string()) // 返回错误信息
}
}
fn main() {
let value = get_number(false).unwrap_or(100); // 失败时使用回退值 100
println!("{}", value);
}
7.2.2 Result 类型别名
Result类型别名的功能是: 可能模块中大多数函数使用的Result中err的类型是相同的, 定义类型别名可以省略编写err类型
pub type Result<T> = result::Result<T, Error>; // Error是err的类型
fn remove(path: &Path) -> Result<i32> { // 使用类型别名, 实际是Result<i32, Error>
...
}
7.2.3 打印错误
常用方法:
- 通过
match
在错误分支下使用println!()
打印err
err
实现了std::error::Error
特型, 可以直接通过println!()
打印, 使用{:?}
得到更详细的信息err.to_string
返回String
类型的错误信息err.source()
返回错误来源信息
writeln!
需要明确指出写入流, 其中stderr()
表示标准错误流let _ = writeln!(stderr(), "error: {}", err);
: 其中_
表示忽略Result
eprintln!
固定stderr()
writeln!
与eprintln!
区别:eprintln!
发生错误会产生panic
7.2.4 传播错误 (传递错误)
不想立即处理错误, 向调用者传播可以使用?
// 与match唯一区别: ?可以自动类型转换, match强制要求类型匹配
let weather = positive(num)?; // 成功: 解包, 失败: 传递给上层调用者(未解包)
相当于
let weather = match get_weather(hometown) {
Ok(success_value) => success_value,
Err(err) => return Err(err) // 传递给上层调用者 Err(err)未解包
};
7.2.5 处理多种Error类型
use std::io::{self, BufRead};
fn read_numbers(file: &mut dyn BufRead) -> Result<Vec<i32>, std::io::Error> {
let mut numbers = vec![];
for line_result in file.lines() {
let line = line_result?; // 读取可能错误
numbers.push(line.parse()?); // 解析整数可能错误
}
Ok(numbers)
}
错误点: line.parse()?
返回的错误类型并不是std::io::Error
解决方法:
- 定义自己的错误类型
SelfError
, 并实现多种错误类型到SelfError
的转换 - 使用
thiserror crate
, 简洁的定义出良好的错误类型 - 更简单的方法: 所有标准库中的错误类型都可以转换为
Box<dyn std::error::Error + Send + Sync + 'static>
dyn std::error::Error
: 表示"任何错误"Send + Sync + 'static
表示可以在线程安全传递
Compiling example1-test v0.1.0 (/Users/lamecrow/TRY/code/rust/rustpg/chapter6/example1-test)
error[E0277]: `?` couldn't convert the error to `std::io::Error`
--> src/main.rs:7:34
|
3 | fn read_numbers(file: &mut dyn BufRead) -> Result<Vec<i32>, std::io::Error> {
| -------------------------------- expected `std::io::Error` because of this
...
7 | numbers.push(line.parse()?); // 解析整数可能错误
| -------^ the trait `From<ParseIntError>` is not implemented for `std::io::Error`, which is required by `Result<Vec<i32>, std::io::Error>: FromResidual<Result<Infallible, _>>`
| |
| this can't be annotated with `?` because it has type `Result<_, ParseIntError>`
标准实现
use std::io::{self, BufRead};
type GenericError = Box<dyn std::error::Error + Send + Sync + 'static>; // 定义错误类型
type GenericResult<T> = Result<t, GenericError>; // 自定义Result
fn read_numbers(file: &mut dyn BufRead) -> GenericResult<i64> {
let mut numbers = vec![];
for line_result in file.lines() {
let line = line_result?; // 读取可能错误
numbers.push(line.parse()?); // 解析整数可能错误
}
Ok(numbers)
}
fn main() {
println!("Lame Crow");
}
7.2.6 处理"不可能发生"的错误
最好的方法是: 使用unwrap()
或者expect(message)
方法, 且一定不会panic
let num = digits.parse::<u64>(); // 确定digit一定是合法数字字符串, 但是仍返回Result
7.2.7 忽略错误
Rust中需要使用_
显式忽略错误
writeln!(stderr(), "error: {}", err); // C风格忽略返回值, 会有warning
let _ = writeln!(stderr(), "error: {}", err); // 使用_显式忽略
7.2.8 处理main()中的错误
将错误传递给调用者是常用的选择, 这也是?
如此简洁的原因.
但是mian无法传递错误, 因为main的返回值通常不是Result
解决方法:
- 使用expect触发panic, 并打印错误消息
- 使main返回Result, 最后返回
Ok(())
, 但是最终错误消息只会显示错误类型 - main中处理错误
fn main() {
func()?; // 编译错误: 因为main的返回值是(), 而不是Result
}
7.2.9 声明自定义错误类型
简单实现
#[derive(Debug, Clone)]
pub struct JsonError {
pub message: String,
pub line: usize,
pub column: usize,
}
可以像标准错误一样工作, 还需要声明fmt
use std::fmt;
#[derive(Debug, Clone)]
pub struct JsonError {
pub message: String,
pub line: usize,
pub column: usize,
}
impl fmt::Display for JsonError {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
write!(f, "{} ({}:{})", self.message, self.line, self.column)
}
} // 原本应该实现Error特型, 但仅仅使用Error的方法只需要定义impl fmt::Display即可
更简单的方法: 使用thiserror crate
use thiserror::Error;
#[derive(Error, Debug)]
#[error("{message:} ({line:}, {column})")] // 输出设置
pub struct JsonError {
message: String,
line: usize,
column: usize,
}
7.2.10 为什么是Result
为什么需要Result:
- Rust要求程序员在每个可能发生错误的地方显式处理错误
- 常见处理方式: 通过
?
传递错误, 且语法非常符合直觉 - 强制要求显式忽略错误, 防止遗漏 (
_
语法) - Result是一种类型, 可以将其转换为对象灵活处理 (比如: 错误量非常大时, 使用
Vec<Result>
批量处理)
代价: Rust对于错误处理需要花费更多的精力