Rust 中的错误处理

2022-09-16 rust language

Rust 中将错误分成了正常的业务错误 Resut 以及崩溃 panic 处理。

简介

对于错误处理,主流分成了两种:A) 异常机制,例如Java、Python等;B) 返回错误,例如GoLang、Rust等。首先看下,通过标准库如何更加优雅的处理异常,总共有如下的几种方式:

  • Option<T> 包括了 Some(value)None
  • Result<T> 包括了 Ok(value)Err(err)
  • panic! 应对非预期的异常,会导致线程退出,同时会做栈展开 (需要设置RUST_BACKTRACE=1环境变量),并自动调用 Drop 方法完成资源释放。
  • abort! 强制异常退出,不返回任何值,不做栈展开,也不会调用相应的析构函数。

Option

在 Rust 中没有类似 null 的空值概念,取而代之的是一个 Option 枚举,用来表达存在性,包含在 prelude 中,可以直接使用,包括了成员 SomeNone 也不需要 Option 前缀。

enum Option<T> {
	None,
	Some(T),
}

其它语言中,在调用一个接口后需要判断返回值是否有效,例如如下示例。

User user = model.getUser();
if (user != null) {
	return;
}
System.out.println("Hello " + user.getName());

只是开发时很容易忘记判断,而且这种逻辑也稍微有些啰嗦,通过 Option 就可以解决这个问题。

fn main() {
    let file = "README.md";
    let ext = match file.find('.') {
        None => None,
        Some(i) => Some(&file[i+1..]),
    };
    if let Some(data) = ext {
        println!("{:?}", data);
    }
}

就是通过编译器强制用户显式处理存在和不存在两种结果,其中的 let Some(x) = xx 是一个 bool 表达式,但是不能单独使用。

上述的方式还是有些复杂,不过好在 Rust 提供了很多算子,这样就可以通过如下方式简化。

fn main() {
    let file = "README.md";
    file.find('.').map(|i| println!("{:?}", &file[i+1..]));
}

常用函数

为了方便处理 Option 返回结果,同时提供了很多常用的函数,可以参考 Enum Option 中的介绍。例如,如果只是测试那么可以使用 unwrap() 方法。

fn main() {
    let file = "README.md";
    let offset = file.find('.').unwrap();
    println!("{:?}", &file[offset+1..]);
}

其中的 unwrap()Option 中实现,如果正常则获取对应的值,否则执行 panic(),所以不太适合线上,可以通过 unwrap_or() 在为 None 时返回一个默认值,还有一个 unwrap_or_else() 通过函数处理为 None 情况。

另外,需要注意,对于 unwrap_or() 无论返回是否为 None 其中的函数都会被调用,也就是 eagerly evaluated,而 unwrap_or_else() 则只有当值是 None 时才会调用,也就是 lazily evaluated

还有个与 unwrap() 类似的 expect() 函数,区别是后者可以指定报错信息,而前者则是默认的值。

let f = File::open("hello.txt").expect("Failed to open hello.txt");

另外,处理 map() 函数,还可以参考 and_then() 算子。

Result

std::result 模块中提供了 Result 枚举类型,可以看做 Option 的加强版,同时还可以返回错误信息,通常用来返回处理结果,其定义如下。

enum Result<T, E> {
    Ok(T),   // 操作成功,并包含操作返回的值
    Err(E),  // 操作失败,同时会包含具体失败的原因
}

如下是一个示例。

use std::str;
use std::result::Result;

fn is_fifty(num: u32) -> Result<u32, &'static str> {
    if num == 50 {
        Ok(num)
    } else {
        Err("It didn't work")
    }
}

fn main() {
    let num = match is_fifty(50) {
        Result::Ok(o) => o,
        Result::Err(..) => {
            panic!("invalid result")
        },
    };
    println!("got value {}", num);
}

还可以通过 type Option<T> = Result<T, ()>; 定义别名,其中的 () 被称为 unit 或者空元组 empty tuple,该 () 类型只对应一个值 (),也就是即使类型也是值。

也可以只处理报错的情况,示例如下。

fn main() {
    let val = "20";
    if let Err(e) = val.parse::<i32>().map(|n| 2 * n) {
        println!("Error: {:?}", e);
    }
}

与 Option 类似,同样包含了 unwrap()unwrap_or()map()and_then() 等算子,同时还有针对错误类型的算子,例如 map_err()or_else() 等,更多函数可以参考 Enum Result 中的介绍。

fn main() {
    let val = "20";
    match val.parse::<i32>().map(|n| 2 * n) {
        Ok(n) => println!("got value {}", n),
        Err(err) => println!("Error: {:?}", err),
    }
}

别名

在很多标准库中,通常是以 Result<i32> 这种方式使用,最常用的就是 io::Result 了,通常为 io::Result<T>,其实就是以别名的方式将错误类型固定下来了,例如如下定义。

use std::num::ParseIntError;
use std::result;

type Result<T> = result::Result<T, ParseIntError>;

fn double(num: &str) -> Result<i32> {
    num.parse::<i32>().map(|n| 2 * n)
}

fn main() {
    match double("20") {
        Ok(n) => println!("got value {}", n),
        Err(err) => println!("Error: {:?}", err),
    }
}

unwrap

如下代码会抛出异常,默认没有打印调用栈,如果要打印,需要添加 RUST_BACKTRACE=1 环境变量。

fn guess(n: i32) -> bool {
    if n < 1 || n > 10 {
        panic!("Invalid number: {}", n);
    }
    n == 5
}
fn main() {
    guess(11);
}

unwrap 的意思就是,根据计算的结果,如果是错误则会直接调用 panic,而且,最常用的 OptionResult 已经实现了该接口。虽然出现异常时会导致程序 panic 退出,但是在示例代码中会大大简化处理逻辑。

另外,还可以通过 ? 问号来简化 OptionResult 的处理,如果有值会将值提取出来,否则返回,此时函数也要求返回 Option 或者 Result 类型。

自定义

很多的库中需要自定义错误,大部分场景需要调用底层的接口,而底层可能对应了不同的错误类型,所以需要同时处理不同的类型的错误转换,例如如下的示例。

#[derive(Debug)]
pub enum Error {
    IO(String),

    // 某个错误里已经包含了详细的错误信息以及上下文
    Parse{source: url::ParserError},
}

// 应用标准接口
impl std::error::Error for Error {}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            Error::IO(msg) => write!(f, "io error: {msg}"),
        }
    }
}

// 处理通过From作隐式转换,还可以通过 map_err 实现
impl From<std::io::Error> for Error {
    fn from(e: std::io::Error) -> Self {
        Error::IO(e.to_string())
    }
}

// 覆盖原有的Result类型,提供默认的Error信息,也可以在某些地方指定具体错误
type Result<T, E = Error> = std::result::Result<T, E>;

标准库

标准库提供了 std::error::Error 接口,其同时继承了 Debug Display 接口,最常见的就是实现 source 接口。

pub trait Error: Debug + Display {
    // 返回底层错误信息
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        None
    }

    // 返回错误的 TypeId 信息,使用默认即可
    fn type_id(&self, _: private::Internal) -> TypeId
    where
        Self: 'static,
    {
        TypeId::of::<Self>()
    }

    // 可选,用于一些基于上下文的错误处理
    fn provide<'a>(&'a self, request: &mut Request<'a>) {}
}

三方库

实践比较常用的是 thiserror 以及 anyhow 两个库,作者相同,分别用于库以及独立应用。其它还可以参考 snafu 这个库。

简单来说,通过 thiserror 可以通过简单的注解定义自己的 emnu struct 类型的错误,而 anyhow 则针对错误提供更加便利的使用或者上下文等信息。如下同时记录 thiserror 常用的技巧:

// 直接从其它库转换
#[derive(Error, Debug)]
pub enum Error {
    #[error(transparent)]
    OtherError(#[from] OtherLibError)
}

其它

类型转换

对于 Option 可以通过 ok_or() 或者 ok_or_else() 转换为 Result 类型。

ResultOption 可以通过自定义的 ok() 函数实现,例如:

fn ok(self) -> Option<T> {
   match self {
     Ok(v) => Some(v),
     Err(_) => None,
   }
}
use std::{fs::File, io::ErrorKind};

fn main() {
    match File::open("some_file_not_exists") {
        Ok(f) => f,
        Err(e) => match e.kind() {
            ErrorKind::NotFound => {
                println!("not found");
                return;
            }
            _ => {
                println!("unknown error {e}");
                return;
            }
        },
    };
}