我知道只要活得足够久, 这种事就一定会发生. ---- 萧伯纳论死亡

本章介绍两类错误:

  • 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=1Rust就会转储当前调用栈
  • 展开调用栈(类似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对于错误处理需要花费更多的精力