The Wayback Machine - https://web.archive.org/web/20200919182850/https://github.com/baoyachi/rust-error-handle
Skip to content
master
Go to file
Code

Latest commit

 

Git stats

Files

Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
src
 
 
 
 
 
 
 
 
 
 
dat
 
 

README.md

细说Rust错误处理

handle-error.png

原文地址:https://github.com/baoyachi/rust-error-handle

1. 前言

这篇文章写得比较长,全文读完大约需要15-20min,如果对Rust的错误处理不清楚或还有些许模糊的同学,请静下心来细细阅读。当读完该篇文章后,可以说对Rust的错误处理可以做到掌握自如。

笔者花费较长篇幅来描述错误处理的来去,详细介绍其及一步步梳理内容,望大家能耐心读完后对大家有所帮助。当然,在写这篇文章之时,也借阅了大量互联网资料,详见链接见底部参考链接

掌握好Rust的错误设计,不仅可以提升我们对错误处理的认识,对代码结构、层次都有很大的帮助。那废话不多说,那我们开启这段阅读之旅吧😄

2. 背景

笔者在写这篇文章时,也翻阅一些资料关于Rust的错误处理资料,多数是对其一笔带过,导致之前接触过其他语言的新同学来说,上手处理Rust的错误会有当头棒喝的感觉。找些资料发现unwrap()也可以解决问题,然后心中暗自窃喜,程序在运行过程中,因为忽略检查或程序逻辑判断,导致某些情况,程序panic。这可能是我们最不愿看到的现象,遂又回到起点,重新去了解Rust的错误处理。

这篇文章,通过一步步介绍,让大家清晰知道Rust的错误处理的究竟。介绍在Rust中的错误使用及如何处理错误,以及在实际工作中关于其使用技巧。

3. unwrap的危害!

下面我们来看一段代码,执行一下:

fn main() {
    let path = "/tmp/dat";
    println!("{}", read_file(path));
}

fn read_file(path: &str) -> String {
    std::fs::read_to_string(path).unwrap()
}

程序执行结果:

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/libcore/result.rs:1188:5
stack backtrace:
   0: backtrace::backtrace::libunwind::trace
             at /Users/runner/.cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.40/src/backtrace/libunwind.rs:88
  ...
  15: rust_sugar::read_file
             at src/main.rs:7
  16: rust_sugar::main
             at src/main.rs:3
  ...
  25: rust_sugar::read_file
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

什么,因为path路径不对,程序竟然崩溃了,这个是我们不能接受的!

unwrap() 这个操作在rust代码中,应该看过很多这种代码,甚至此时我们正在使用它。它主要用于OptionResult的打开其包装的结果。常常我们在代码中,使用简单,或快速处理,使用了 unwrap() 的操作,但是,它是一个非常危险的信号!

可能因为没有程序检查或校验,潜在的bug可能就出现其中,使得我们程序往往就panic了。这可能使我们最不愿看到的现象。

在实际项目开发中,程序中可能充斥着大量代码,我们很难避免unwrap()的出现,为了解决这种问题,我们通过做code review,或使用脚本工具检查其降低其出现的可能性。

通常每个项目都有一些约束,或许:在大型项目开发中, 不用unwrap() 方法,使用其他方式处理程序,unwrap() 的不出现可能会使得程序的健壮性高出很多。

这里前提是团队或大型项目,如果只是写一个简单例子(demo)就不在本篇文章的讨论范畴。因为一个Demo的问题,可能只是快速示范或演示,不考虑程序健壮性, unwrap() 的操作可能会更方便代码表达。

可能有人会问,我们通常跑程序unit test,其中的很多mock数据会有 unwrap() 的操作,我们只是为了在单元测试中使得程序简单。这种也能不使用吗?答案:是的,完全可以不使用 unwrap() 也可以做到的。

4. 对比语言处理错误

说到unwrap(),我们不得不提到rust的错误处理,unwrap()Rust的错误处理是密不可分的。

4.1 golang的错误处理演示

如果了解golang的话,应该清楚下面这段代码的意思:

package main

import (
    "io/ioutil"
    "log"
)

func main() {
    path := "/tmp/dat"  //文件路径
    file, err := readFile(path) 
    if err != nil {
        log.Fatal(err) //错误打印
    }
    println("%s", file) //打印文件内容
}

func readFile(path string) (string, error) {
    dat, err := ioutil.ReadFile(path)  //读取文件内容
    if err != nil {  //判断err是否为nil
        return "", err  //不为nil,返回err结果
    }
    return string(dat), nil  //err=nil,返回读取文件内容
}

我们执行下程序,打印如下。执行错误,当然,因为我们给的文件路径不存在,程序报错。

2020/02/24 01:24:04 open /tmp/dat: no such file or directory

这里,golang采用多返回值方式,程序报错返回错误问题,通过判断 err!=nil 来决定程序是否继续执行或终止该逻辑。当然,如果接触过golang项目时,会发现程序中大量充斥着if err!=nil的代码,对此网上有对if err!=nil进行了很多讨论,因为这个不在本篇文章的范畴中,在此不对其追溯、讨论。

4.2 Rust 错误处理示例

对比了golang代码,我们对照上面的例子,看下在Rust中如何编写这段程序,代码如下:

fn main() {
    let path = "/tmp/dat";  //文件路径
    match read_file(path) { //判断方法结果
        Ok(file) => { println!("{}", file) } //OK 代表读取到文件内容,正确打印文件内容
        Err(e) => { println!("{} {}", path, e) } //Err代表结果不存在,打印错误结果
    }
}

fn read_file(path: &str) -> Result<String,std::io::Error> { //Result作为结果返回值
    std::fs::read_to_string(path) //读取文件内容
}

当前,因为我们给的文件路径不存在,程序报错,打印内容如下:

No such file or directory (os error 2)

Rust代表中,Result是一个enum枚举对象,部分源码如下:

pub enum Result<T, E> {
    /// Contains the success value
    Ok(#[stable(feature = "rust1", since = "1.0.0")] T),

    /// Contains the error value
    Err(#[stable(feature = "rust1", since = "1.0.0")] E),
}

通常我们使用Result的枚举对象作为程序的返回值,通过Result来判断其结果,我们使用match匹配的方式来获取Result的内容,判断正常(Ok)或错误(Err)。

或许,我们大致向上看去,golang代码和Rust代码没有本质区别,都是采用返回值方式,给出程序结果。下面我们就对比两种语言说说之间区别:

  • golang采用多返回值方式,我们在拿到目标结果时(上面是指文件内容file),需要首先对err判断是否为nil,并且我们在return时,需要给多返回值分别赋值,调用时需要对 if err!=nil 做结果判断。
  • Rust中采用Result的枚举对象做结果返回。枚举的好处是:多选一。因为Result的枚举类型为OkErr,使得我们每次在返回Result的结果时,要么是Ok,要么是Err。它不需要return结果同时给两个值赋值,这样的情况只会存在一种可能性: Ok or Err
  • golang的函数调用需要对 if err!=nil做结果判断,因为这段代码 判断是手动逻辑,往往我们可能因为疏忽,导致这段逻辑缺失,缺少校验。当然,我们在编写代码期间可以通过某些工具 lint 扫描出这种潜在bug。
  • Rustmatch判断是自动打开,当然你也可以选择忽略其中某一个枚举值,我们不在此说明。

可能有人发现,如果我有多个函数,需要多个函数的执行结果,这样需要match代码多次,代码会不会是一坨一坨,显得代码很臃肿,难看。是的,这个问题提出的的确是有这种问题,不过这个在后面我们讲解的时候,会通过程序语法糖避免多次match多次结果的问题,不过我们在此先不叙说,后面将有介绍。

5. Rust中的错误处理

前面不管是golang还是Rust采用return返回值方式,两者都是为了解决程序中错误处理的问题。好了,前面说了这么多,我们还是回归正题:Rust中是如何对错误进行处理的?

要想细致了解Rust的错误处理,我们需要了解std::error::Error,该trait的内部方法,部分代码如下: 参考链接:https://doc.rust-lang.org/std/error/trait.Error.html

pub trait Error: Debug + Display {

    fn description(&self) -> &str {
        "description() is deprecated; use Display"
    }

    #[rustc_deprecated(since = "1.33.0", reason = "replaced by Error::source, which can support \
                                                   downcasting")]

    fn cause(&self) -> Option<&dyn Error> {
        self.source()
    }

    fn source(&self) -> Option<&(dyn Error + 'static)> { None }

    #[doc(hidden)]
    fn type_id(&self, _: private::Internal) -> TypeId where Self: 'static {
        TypeId::of::<Self>()
    }

    #[unstable(feature = "backtrace", issue = "53487")]
    fn backtrace(&self) -> Option<&Backtrace> {
        None
    }
}
  • description()在文档介绍中,尽管使用它不会导致编译警告,但新代码应该实现impl Display ,新impl的可以省略,不用实现该方法, 要获取字符串形式的错误描述,请使用to_string()

  • cause()1.33.0被抛弃,取而代之使用source()方法,新impl的不用实现该方法。

  • source()此错误的低级源,如果内部有错误类型Err返回:Some(e),如果没有返回:None

    • 如果当前Error是低级别的Error,并没有子Error,需要返回None。介于其本身默认有返回值None,可以不覆盖该方法。
    • 如果当前Error包含子Error,需要返回子ErrorSome(err),需要覆盖该方法。
  • type_id()该方法被隐藏。

  • backtrace()返回发生此错误的堆栈追溯,因为标记unstable,在Ruststable版本不被使用。

  • 自定义的Error需要impl std::fmt::Debug的trait,当然我们只需要在默认对象上添加注解:#[derive(Debug)]即可。

总结一下,自定义一个error需要实现如下几步:

  • 手动实现impl std::fmt::Display的trait,并实现 fmt(...)方法。
  • 手动实现impl std::fmt::Debugtrait,一般直接添加注解即可:#[derive(Debug)]
  • 手动实现impl std::error::Errortrait,并根据自身error级别是否覆盖std::error::Error中的source()方法。

下面我们自己手动实现下Rust自定义错误:CustomError

use std::error::Error;

///自定义类型 Error,实现std::fmt::Debug的trait
#[derive(Debug)]
struct CustomError {
    err: ChildError,
}

///实现Display的trait,并实现fmt方法
impl std::fmt::Display for CustomError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "CustomError is here!")
    }
}

///实现Error的trait,因为有子Error:ChildError,需要覆盖source()方法,返回Some(err)
impl std::error::Error for CustomError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        Some(&self.err)
    }
}


///子类型 Error,实现std::fmt::Debug的trait
#[derive(Debug)]
struct ChildError;

///实现Display的trait,并实现fmt方法
impl std::fmt::Display for ChildError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "ChildError is here!")
    }
}

///实现Error的trait,因为没有子Error,不需要覆盖source()方法
impl std::error::Error for ChildError {}

///构建一个Result的结果,返回自定义的error:CustomError
fn get_super_error() -> Result<(), CustomError> {
    Err(CustomError { err: ChildError })
}

fn main() {
    match get_super_error() {
        Err(e) => {
            println!("Error: {}", e);
            println!("Caused by: {}", e.source().unwrap());
        }
        _ => println!("No error"),
    }
}
  • ChildError为子类型Error,没有覆盖source()方法,空实现了std::error::Error
  • CustomError有子类型ChildError,覆盖source(),并返回了子类型Option值:Some(&self.err)

运行执行结果,显示如下:

Error: CustomError is here!
Caused by: ChildError is here!

至此,我们就了解了如何实现Rust自定义Error了。

6. 自定义Error转换:From

上面我们说到,函数返回Result的结果时,需要获取函数的返回值是成功(Ok)还是失败(Err),需要使用match匹配,我们看下多函数之间调用是如何解决这类问题的?假设我们有个场景:

  • 读取一文件
  • 将文件内容转化为UTF8格式
  • 将转换后格式内容转为u32的数字。

所以我们有了下面三个函数(省略部分代码):

...

///读取文件内容
fn read_file(path: &str) -> Result<String, std::io::Error> {
    std::fs::read_to_string(path)
}

/// 转换为utf8内容
fn to_utf8(v: &[u8]) -> Result<&str, std::str::Utf8Error> {
    std::str::from_utf8(v)
}

/// 转化为u32数字
fn to_u32(v: &str) -> Result<u32, std::num::ParseIntError> {
    v.parse::<u32>()
}

最终,我们得到u32的数字,对于该场景如何组织我们代码呢?

  • unwrap()直接打开三个方法,取出值。这种方式太暴力,并且会有bug,造成程序panic,不被采纳。
  • match匹配,如何返回OK,继续下一步,否则报错终止逻辑,那我们试试。

参考代码如下:

fn main() {
    let path = "./dat";
    match read_file(path) {
        Ok(v) => {
            match to_utf8(v.as_bytes()) {
                Ok(u) => {
                    match to_u32(u) {
                        Ok(t) => {
                            println!("num:{:?}", u);
                        }
                        Err(e) => {
                            println!("{} {}", path, e)
                        }
                    }
                }
                Err(e) => {
                    println!("{} {}", path, e)
                }
            }
        }
        Err(e) => {
            println!("{} {}", path, e)
        }
    }
}

///读取文件内容
fn read_file(path: &str) -> Result<String, std::io::Error> {
    std::fs::read_to_string(path)
}

/// 转换为utf8内容
fn to_utf8(v: &[u8]) -> Result<&str, std::str::Utf8Error> {
    std::str::from_utf8(v)
}

/// 转化为u32数字
fn to_u32(v: &str) -> Result<u32, std::num::ParseIntError> {
    v.parse::<u32>()
}

天啊,虽然是实现了上面场景的需求,但是代码犹如叠罗汉,程序结构越来越深啊,这个是我们没法接受的!match匹配导致程序如此不堪一击。�那么有没有第三种方法呢?当然是有的:From转换。

前面我们说到如何自定义的Error,如何我们将上面三个error收纳到我们自定义的Error中,将它们三个Error变成自定义Error子Error,这样我们对外的Result统一返回自定义的Error。这样程序应该可以改变点什么,我们来试试吧。

#[derive(Debug)]
enum CustomError {
    ParseIntError(std::num::ParseIntError),
    Utf8Error(std::str::Utf8Error),
    IoError(std::io::Error),
}
impl std::error::Error for CustomError{
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match &self {
            CustomError::IoError(ref e) => Some(e),
            CustomError::Utf8Error(ref e) => Some(e),
            CustomError::ParseIntError(ref e) => Some(e),
        }
    }
}

impl Display for CustomError{
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match &self {
            CustomError::IoError(ref e) => e.fmt(f),
            CustomError::Utf8Error(ref e) => e.fmt(f),
            CustomError::ParseIntError(ref e) => e.fmt(f),
        }
    }
}

impl From<ParseIntError> for CustomError {
    fn from(s: std::num::ParseIntError) -> Self {
        CustomError::ParseIntError(s)
    }
}

impl From<IoError> for CustomError {
    fn from(s: std::io::Error) -> Self {
        CustomError::IoError(s)
    }
}

impl From<Utf8Error> for CustomError {
    fn from(s: std::str::Utf8Error) -> Self {
        CustomError::Utf8Error(s)
    }
}
  • CustomError为我们实现的自定义Error
  • CustomError有三个子类型Error
  • CustomError分别实现了三个子类型Error From的trait,将其类型包装为自定义Error的子类型

好了,有了自定义的CustomError,那怎么使用呢? 我们看代码:

use std::io::Error as IoError;
use std::str::Utf8Error;
use std::num::ParseIntError;
use std::fmt::{Display, Formatter};


fn main() -> std::result::Result<(),CustomError>{
    let path = "./dat";
    let v = read_file(path)?;
    let x = to_utf8(v.as_bytes())?;
    let u = to_u32(x)?;
    println!("num:{:?}",u);
    Ok(())
}

///读取文件内容
fn read_file(path: &str) -> std::result::Result<String, std::io::Error> {
    std::fs::read_to_string(path)
}

/// 转换为utf8内容
fn to_utf8(v: &[u8]) -> std::result::Result<&str, std::str::Utf8Error> {
    std::str::from_utf8(v)
}

/// 转化为u32数字
fn to_u32(v: &str) -> std::result::Result<u32, std::num::ParseIntError> {
    v.parse::<u32>()
}


#[derive(Debug)]
enum CustomError {
    ParseIntError(std::num::ParseIntError),
    Utf8Error(std::str::Utf8Error),
    IoError(std::io::Error),
}
impl std::error::Error for CustomError{
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match &self {
            CustomError::IoError(ref e) => Some(e),
            CustomError::Utf8Error(ref e) => Some(e),
            CustomError::ParseIntError(ref e) => Some(e),
        }
    }
}

impl Display for CustomError{
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match &self {
            CustomError::IoError(ref e) => e.fmt(f),
            CustomError::Utf8Error(ref e) => e.fmt(f),
            CustomError::ParseIntError(ref e) => e.fmt(f),
        }
    }
}

impl From<ParseIntError> for CustomError {
    fn from(s: std::num::ParseIntError) -> Self {
        CustomError::ParseIntError(s)
    }
}

impl From<IoError> for CustomError {
    fn from(s: std::io::Error) -> Self {
        CustomError::IoError(s)
    }
}

impl From<Utf8Error> for CustomError {
    fn from(s: std::str::Utf8Error) -> Self {
        CustomError::Utf8Error(s)
    }
}

其实我们主要关心的是这段代码:

fn main() -> Result<(),CustomError>{
    let path = "./dat";
    let v = read_file(path)?;
    let x = to_utf8(v.as_bytes())?;
    let u = to_u32(x)?;
    println!("num:{:?}",u);
    Ok(())
}

我们使用了?来替代原来的match匹配的方式。?使用问号作用在函数的结束,意思是:

  • 程序接受了一个Result<(),CustomError>自定义的错误类型。
  • 当前如果函数结果错误,程序自动抛出Err自身错误类型,并包含相关自己类型错误信息,因为我们做了From转换的操作,该函数的自身类型错误会通过实现的From操作自动转化为CustomError的自定义类型错误。
  • 当前如果函数结果正确,继续之后逻辑,直到程序结束。

这样,我们通过From?解决了之前match匹配代码层级深的问题,因为这种转换是无感知的,使得我们在处理好错误类型后,只需要关心我们的目标值即可,这样不需要显示对Err(e)的数据单独处理,使得我们在函数后添加?后,程序一切都是自动了。

还记得我们之前讨论在对比golang的错误处理时的:if err!=nil的逻辑了吗,这种因为用了?语法糖使得该段判断将不再存在。

另外,我们还注意到,Result的结果可以作用在main函数上,

  • 是的,Result的结果不仅能作用在main函数上
  • Result还可以作用在单元测试上,这就是我们文中刚开始提到的:因为有了Result的作用,使得我们在程序中几乎可以完全摒弃unwrap()的代码块,使得程序更轻,大大减少潜在问题,程序组织结构更加清晰。

下面这是作用在单元测试上的Result的代码:

...

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_get_num() -> std::result::Result<(), CustomError> {
        let path = "./dat";
        let v = read_file(path)?;
        let x = to_utf8(v.as_bytes())?;
        let u = to_u32(x)?;
        assert_eq!(u, 8);
        Ok(())
    }
}

7. 重命名Result

我们在实际项目中,会大量使用如上的Result结果,并且ResultErr类型是我们自定义错误,导致我们写程序时会显得非常啰嗦冗余

///读取文件内容
fn read_file(path: &str) -> std::result::Result<String, CustomError> {
    let val = std::fs::read_to_string(path)?;
    Ok(val)
}

/// 转换为utf8内容
fn to_utf8(v: &[u8]) -> std::result::Result<&str, CustomError> {
    let x = std::str::from_utf8(v)?;
    Ok(x)
}

/// 转化为u32数字
fn to_u32(v: &str) -> std::result::Result<u32, CustomError> {
    let i = v.parse::<u32>()?;
    Ok(i)
}

我们的程序中,会大量充斥着这种模板代码Rust本身支持对类型自定义,使得我们只需要重命名Result即可:

pub type IResult<I> = std::result::Result<I, CustomError>; ///自定义Result类型:IResult

这样,凡是使用的是自定义类型错误的Result都可以使用IResult来替换std::result::Result的类型,使得简化程序,隐藏Error类型及细节,关注目标主体,代码如下:

///读取文件内容
fn read_file(path: &str) -> IResult<String> {
    let val = std::fs::read_to_string(path)?;
    Ok(val)
}

/// 转换为utf8内容
fn to_utf8(v: &[u8]) -> IResult<&str> {
    let x = std::str::from_utf8(v)?;
    Ok(x)
}

/// 转化为u32数字
fn to_u32(v: &str) -> IResult<u32> {
    let i = v.parse::<u32>()?;
    Ok(i)
}

std::result::Result<I, CustomError> 替换为:IResult<I>类型

当然,会有人提问,如果是多参数类型怎么处理呢,同样,我们只需将OK类型变成 tuple (I,O)类型的多参数数据即可,大概这样:

pub type IResult<I, O> = std::result::Result<(I, O), CustomError>;

使用也及其简单,只需要返回:I,O的具体类型,举个示例:

fn foo() -> IResult<String, u32> {
    Ok((String::from("bar"), 32))
}

使用重命名类型的Result,使得我们错误类型统一,方便处理。在实际项目中,可以大量看到这种例子的存在。

8. Option转换

我们知道,在Rust中,需要使用到unwrap()的方法的对象有Result,Option对象。我们看下Option的大致结构:

pub enum Option<T> {
    /// No value
    #[stable(feature = "rust1", since = "1.0.0")]
    None,
    /// Some value `T`
    #[stable(feature = "rust1", since = "1.0.0")]
    Some(#[stable(feature = "rust1", since = "1.0.0")] T),
}

Option本身是一个enum对象,如果该函数(方法)调用结果值没有值,返回None,反之有值返回Some(T)

如果我们想获取Some(T)中的T,最直接的方式是:unwrap()。我们前面说过,使用unwrap()的方式太过于暴力,如果出错,程序直接panic,这是我们最不愿意看到的结果。

Ok,那么我们试想下, 利用Option能使用?语法糖吗?如果能用?转换的话,是不是代码结构就更简单了呢?我们尝试下,代码如下:

#[derive(Debug)]
enum Error {
    OptionError(String),
}

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::OptionError(ref e) => e.fmt(f),
        }
    }
}

pub type Result<I> = std::result::Result<I, Error>;


fn main() -> Result<()> {
    let bar = foo(60)?;
    assert_eq!("bar", bar);
    Ok(())
}

fn foo(index: i32) -> Option<String> {
    if index > 60 {
        return Some("bar".to_string());
    }
    None
}

执行结果报错:

error[E0277]: `?` couldn't convert the error to `Error`
  --> src/main.rs:22:22
   |
22 |     let bar = foo(60)?;
   |                      ^ the trait `std::convert::From<std::option::NoneError>` is not implemented for `Error`
   |
   = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
   = note: required by `std::convert::From::from`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
error: could not compile `hyper-define`.

提示告诉我们没有转换std::convert::From<std::option::NoneError>,但是NoneError本身是unstable,这样我们没法通过From转换为自定义Error

本身,在Rust的设计中,关于OptionResult就是一对孪生兄弟一样的存在,Option的存在可以忽略异常的细节,直接关注目标主体。当然,Option也可以通过内置的组合器ok_or()方法将其变成Result。我们大致看下实现细节:

impl<T> Option<T> {
    pub fn ok_or<E>(self, err: E) -> Result<T, E> {
        match self {
            Some(v) => Ok(v),
            None => Err(err),
        }
    }
}    

这里通过ok_or()方法通过接收一个自定义Error类型,将一个Option->Result。好的,变成Result的类型,我们就是我们熟悉的领域了,这样处理起来就很灵活。

关于Option的其他处理方式,不在此展开解决,详细的可看下面链接:

延伸链接:https://stackoverflow.com/questions/59568278/why-does-the-operator-report-the-error-the-trait-bound-noneerror-error-is-no

9. 避免unwrap()

有人肯定会有疑问,如果需要判断的逻辑,又不用?这种操作,怎么取出OptionResult的数据呢,当然点子总比办法多,我们来看下Option如何做的:

fn main() {
    if let Some(v) = opt_val(60) {
        println!("{}", v);
    }
}

fn opt_val(num: i32) -> Option<String> {
    if num >= 60 {
        return Some("foo bar".to_string());
    }
    None
}

是的,我们使用if let Some(v)的方式取出值,当前else的逻辑就可能需要自己处理了。当然,Option可以这样做,Result也一定可以:

fn main() {
    if let Ok(v) = read_file("./dat") {
        println!("{}", v);
    }
}

fn read_file(path: &str) -> Result<String, std::io::Error> {
    std::fs::read_to_string(path)
}

只不过,在处理Result的判断时,使用的是if let Ok(v),这个和Optionif let Some(v)有所不同。

到这里,unwrap()的代码片在项目中应该可以规避了。补充下,这里强调了几次规避,就如前所言:团队风格统一,方便管理代码,消除潜在危机

10. 自定义Error同级转换

我们在项目中,一个函数(方法)内部会有多次Result的结果判断:?,假设我们自定义的全局Error名称为:GlobalError

这时候,如果全局有一个Error可能就会出现如下错误:

std::convert::From<error::GlobalError<A>>` is not implemented for `error::GlobalError<B>

意思是:我们自定义的GlobalError没有通过From<GlobalError>转换我们自己自定义的GlobalError,那这样,就等于自己转换自己。注意:

  • 第一:这是我们不期望这样做的。
  • 第二:遇到这种自己转换自己的T类型很多,我们不可能把出现的T类型通通实现一遍。 这时候,我们考虑自定义另一个Error了,假设我们视为:InnnerError,我们全局的Error取名为:GlobalError,我们在遇到上面错误时,返回Result<T,InnerError>,这样我们遇到Result<T,GlobalError>时,只需要通过From<T>转换即可,代码示例如下:
impl From<InnerError> for GlobalError {
    fn from(s: InnerError) -> Self {
        Error::new(ErrorKind::InnerError(e))
    }
}

上面说的这种情况,可能会在项目中出现多个自定义Error,出现这种情况时,存在多个不同Error的std::result::Result<T,Err>的返回。这里的Err就可以根据我们业务现状分别反回不同类型了。最终,只要实现了From<T>trait可转化为最终期望结果。

11. Error常见开源库

好了,介绍到这里,我们应该有了非常清晰的认知:关于如何处理Rust的错误处理问题了。但是想想上面的这些逻辑多数是模板代码,我们在实际中,大可不必这样。说到这里,开源社区也有了很多对错误处理库的支持,下面列举了一些:

12. 参考链接

13 错误处理实战

这个例子介绍了如何在https://github.com/Geal/nom中处理错误,这里就不展开介绍了,有兴趣的可自行阅读代码。

详细见链接:https://github.com/baoyachi/rust-error-handle/blob/master/src/demo_nom_error_handle.rs

14. 总结

好了,经过上面的长篇大论,不知道大家是否明白如何自定义处理Error呢了。大家现在带着之前的已有的问题或困惑,赶紧实战下Rust的错误处理吧,大家有疑问或者问题都可以留言我,希望这篇文章对你有帮助。

文中代码详见:https://github.com/baoyachi/rust-handle-error/tree/master/src

原文地址:https://github.com/baoyachi/rust-error-handle

Releases

No releases published

Packages

No packages published

Languages

You can’t perform that action at this time.