Rust:如何从容面对错误处理

错误处理并不是简单的使用Result和Option。Rust中的错误处理对于初学者来说并不友好,反复遭到其蹂躏后,决定整理下错误处理相关的知识。内容主要有两部分,一部分是官方提供了一些跟Result相关的方法,另一部分则介绍如何定义和处理错误。掌握这些知识让能你不再惧怕rust的错误处理。

错误处理的相关方法

如果想从容处理错误,需要结合官方提供的方法来使用。 事半功倍

  • or()
  • and()
  • or_else()
  • and_then()
  • map()
  • map_err()
  • map_or()
  • map_or_else()
  • ok_or()
  • ok_or_else()
  • ...

下面就由浅入深的讲解什么时候会用到这些工具,以及怎么用,以及最终设计程序写代码的时候如何设计Err类型

or(),and()

两者选其一使用or(),and(),

  • or(),表达式按顺序求值。 如果任何表达式的结果是 Some 或 Ok,则该值将立即返回。

  • and(),如果两个表达式的结果都是Some或Ok,则返回第二个表达式中的值。 如果任一结果为 None 或 Err,则立即返回。

    rust 复制代码
      let s1 = Some("some1");
      let s2 = Some("some2");
      let n: Option<&str> = None;
    
      let o1: Result<&str, &str> = Ok("ok1");
      let o2: Result<&str, &str> = Ok("ok2");
      let e1: Result<&str, &str> = Err("error1");
      let e2: Result<&str, &str> = Err("error2");
    
      assert_eq!(s1.or(s2), s1); // Some1 or Some2 = Some1
      assert_eq!(s1.or(n), s1); // Some or None = Some
      assert_eq!(n.or(s1), s1); // None or Some = Some
      assert_eq!(n.or(n), n); // None1 or None2 = None2
    
      assert_eq!(o1.or(o2), o1); // Ok1 or Ok2 = Ok1
      assert_eq!(o1.or(e1), o1); // Ok or Err = Ok
      assert_eq!(e1.or(o1), o1); // Err or Ok = Ok
      assert_eq!(e1.or(e2), e2); // Err1 or Err2 = Err2
    
      assert_eq!(s1.and(s2), s2); // Some1 and Some2 = Some2
      assert_eq!(s1.and(n), n); // Some and None = None
      assert_eq!(n.and(s1), n); // None and Some = None
      assert_eq!(n.and(n), n); // None1 and None2 = None1
    
      assert_eq!(o1.and(o2), o2); // Ok1 and Ok2 = Ok2
      assert_eq!(o1.and(e1), e1); // Ok and Err = Err
      assert_eq!(e1.and(o1), e1); // Err and Ok = Err
      assert_eq!(e1.and(e2), e1); // Err1 and Err2 = Err1

or_else(), and_then()

or() and() 只是想简单的选择,不能修改其中的值,如果想在里面进行复杂的逻辑的话就需要闭包 or_else(), and_then(),

rust 复制代码
    // or_else with Option
    let s1 = Some("some1");
    let s2 = Some("some2");
    let fn_some = || Some("some2"); // 类似于: let fn_some = || -> Option<&str> { Some("some2") };

    let n: Option<&str> = None;
    let fn_none = || None;

    assert_eq!(s1.or_else(fn_some), s1); // Some1 or_else Some2 = Some1
    assert_eq!(s1.or_else(fn_none), s1); // Some or_else None = Some
    assert_eq!(n.or_else(fn_some), s2); // None or_else Some = Some
    assert_eq!(n.or_else(fn_none), None); // None1 or_else None2 = None2

    // or_else with Result
    let o1: Result<&str, &str> = Ok("ok1");
    let o2: Result<&str, &str> = Ok("ok2");
    let fn_ok = |_| Ok("ok2"); // 类似于: let fn_ok = |_| -> Result<&str, &str> { Ok("ok2") };

    let e1: Result<&str, &str> = Err("error1");
    let e2: Result<&str, &str> = Err("error2");
    let fn_err = |_| Err("error2");

    assert_eq!(o1.or_else(fn_ok), o1); // Ok1 or_else Ok2 = Ok1
    assert_eq!(o1.or_else(fn_err), o1); // Ok or_else Err = Ok
    assert_eq!(e1.or_else(fn_ok), o2); // Err or_else Ok = Ok
    assert_eq!(e1.or_else(fn_err), e2); // Err1 or_else Err2 = Err2

map()

如果相对Resut Option 中的数值进行修改使用 map()

rust 复制代码
   let s1 = Some("abcde");
   let s2 = Some(5);

   let n1: Option<&str> = None;
   let n2: Option<usize> = None;

   let o1: Result<&str, &str> = Ok("abcde");
   let o2: Result<usize, &str> = Ok(5);

   let e1: Result<&str, &str> = Err("abcde");
   let e2: Result<usize, &str> = Err("abcde");

   let fn_character_count = |s: &str| s.chars().count();

   assert_eq!(s1.map(fn_character_count), s2); // Some1 map = Some2
   assert_eq!(n1.map(fn_character_count), n2); // None1 map = None2

   assert_eq!(o1.map(fn_character_count), o2); // Ok1 map = Ok2
   assert_eq!(e1.map(fn_character_count), e2); // Err1 map = Err2

map_err()

如果相对Resut 中的err进行修改使用 map_err()

rust 复制代码
 let o1: Result<&str, &str> = Ok("abcde");
 let o2: Result<&str, isize> = Ok("abcde");

 let e1: Result<&str, &str> = Err("404");
 let e2: Result<&str, isize> = Err(404);

 let fn_character_count = |s: &str| -> isize { s.parse().unwrap() }; // 该函数返回一个 isize

 assert_eq!(o1.map_err(fn_character_count), o2); // Ok1 map = Ok2
 assert_eq!(e1.map_err(fn_character_count), e2); // Err1 map = Err2

map_or()

如果确定不会返回err,可以使用 map_or() 返回一个默认值,而不是返回err

rust 复制代码
  const V_DEFAULT: u32 = 1;

  let s: Result<u32, ()> = Ok(10);
  let n: Option<u32> = None;
  let fn_closure = |v: u32| v + 2;

  assert_eq!(s.map_or(V_DEFAULT, fn_closure), 12);
  assert_eq!(n.map_or(V_DEFAULT, fn_closure), V_DEFAULT);

map_or_else()

map_or只能返回默认的值,如果想使用闭包提供一个默认值可以使用map_or_else()

ini 复制代码
    let s = Some(10);
    let n: Option<i8> = None;

    let fn_closure = |v: i8| v + 2;
    let fn_default = || 1;

    assert_eq!(s.map_or_else(fn_default, fn_closure), 12);
    assert_eq!(n.map_or_else(fn_default, fn_closure), 1);

    let o = Ok(10);
    let e = Err(5);
    let fn_default_for_result = |v: i8| v + 1; // 闭包可以对 Err 中的值进行处理,并返回一个新值

    assert_eq!(o.map_or_else(fn_default_for_result, fn_closure), 12);
    assert_eq!(e.map_or_else(fn_default_for_result, fn_closure), 6);

ok_or()

如果想将Option转换为Result可以使用 ok_or()

rust 复制代码
    const ERR_DEFAULT: &str = "error message";

    let s = Some("abcde");
    let n: Option<&str> = None;

    let o: Result<&str, &str> = Ok("abcde");
    let e: Result<&str, &str> = Err(ERR_DEFAULT);

    assert_eq!(s.ok_or(ERR_DEFAULT), o); // Some(T) -> Ok(T)
    assert_eq!(n.ok_or(ERR_DEFAULT), e); // None -> Err(default)

ok_or_else()

但是在面对result是err的时候,想要返回同类型的err就需要传入一个闭包,这时候需要使用 ok_or_else() ``` let s = Some("abcde"); let n: Option<&str> = None; let fn_err_message = || "error message";

rust 复制代码
let o: Result<&str, &str> = Ok("abcde");
let e: Result<&str, &str> = Err("error message");

assert_eq!(s.ok_or_else(fn_err_message), o); // Some(T) -> Ok(T)
assert_eq!(n.ok_or_else(fn_err_message), e); // None -> Err(default)
```

Error怎么设计

通常新手玩家在面对形形色色的result的时候总会抓狂,比如调用不同Result返回值的时候总是报错,说类型不兼容。。。十分要命,通过弄清楚Result类型,就再也不会担心写代码的时候卡在Result了

自定义简单的错误

通常我们在写我们的程序的时候,总是会自定义一些出现的错误,首先先来认识一下Result,写一个自定义的 最简单的 Result

rust 复制代码
use std::fmt;

// CustomError 是自定义错误类型,它可以是当前包中定义的任何类型,在这里为了简化,我们使用了单元结构体作为例子。
// 为 CustomError 自动派生 Debug 特征,目的是为了可以使用 {:?} 进行展现
#[derive(Debug)]
struct CustomError;

// 为 CustomError 实现 std::fmt::Display 特征,目的是为了可以使用 {} 进行展现
impl fmt::Display for CustomError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "An Error Occurred, Please Try Again!") // user-facing output
    }
}

// 一个示例函数用于产生 CustomError 错误
fn make_error() -> Result<(), CustomError> {
    Err(CustomError)
}

fn main(){
    match make_error() {
        Err(e) => eprintln!("{}", e),
        _ => println!("No error"),
    }

    eprintln!("{:?}", make_error()); // Err({ file: src/main.rs, line: 17 })
}

不要在意 eprintln!,他的作用在这里和println!作用是一样的,只有在重定向输出的时候才会有区别,详细可以参考这篇文章:

实现Debug和Display一方面是为了能够显错误,毕竟不能显示,那这个错误定义的也没有意义,但是更更更重要的是只有实现了Debug和Display才可以将自定义错误转换成 Box<dyn std::error:Error> 特征对象。

自定义复杂点的错误

当然上面的确实太简单了,在实际工作中我们通常会给错误一个错误码和错误信息

rust 复制代码
use std::fmt;

struct CustomError {
    code: usize,
    message: String,
}

// 根据错误码显示不同的错误信息
impl fmt::Display for CustomError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let err_msg = match self.code {
            404 => "Sorry, Can not find the Page!",
            _ => "Sorry, something is wrong! Please Try Again!",
        };

        write!(f, "{}", err_msg)
    }
}

impl fmt::Debug for CustomError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "CustomError {{ code: {}, message: {} }}",
            self.code, self.message
        )
    }
}

fn make_error() -> Result<(), CustomError> {
    Err(CustomError {
        code: 404,
        message: String::from("Page not found"),
    })
}

fn main() {
    match make_error() {
        Err(e) => eprintln!("{}", e), // 抱歉,未找到指定的页面!
        _ => println!("No error"),
    }

    eprintln!("{:?}", make_error()); // Err(CustomError { code: 404, message: Page not found })

    eprintln!("{:#?}", make_error());
    // Err(
    //     CustomError { code: 404, message: Page not found }
    // )
}

相比较于上面简单的例子我们同时手动实现了Display和Debug,目的是戈伊耿定制化的实现我们想要的展示效果,使用 #[derive(Debug)]提供的只是默认的展示。

错误转换

好了,现在我们学会了定义自己程序中的Err,但是呢,我们通常会使用各种第三方库,每个库中定义的错误都不尽相同,如何将他们定义的错误招安成自己的错误呢?要不然各种错误类型弄起来总是乱七八糟的。

为了解决这个问题,官方提供了std::convert::From trait用来对错误进行转换

rust 复制代码
use std::fs::File;
use std::io;

#[derive(Debug)]
struct CustomError {
   kind: String,    // 错误类型
   message: String, // 错误信息
}

// 为 CustomError 实现 std::convert::From 特征,由于 From 包含在 std::prelude 中,因此可以直接简化引入。
// 实现 From<io::Error> 意味着我们可以将 io::Error 错误转换成自定义的 CustomError 错误
impl From<io::Error> for CustomError {
   fn from(error: io::Error) -> Self {
       CustomError {
           kind: String::from("io"),
           message: error.to_string(),
       }
   }
}

fn main() -> Result<(), CustomError> {
   let _file = File::open("nonexistent_file.txt")?;

   Ok(())
}


//Error: CustomError { kind: "io", message: "No such file or directory (os error 2)" }

在上面必要重要的地方就是使用?后将std::io::Error自动转换成了我们的CustomError,是不是很方便? 代码写多了,你就会发现rust写起来真滴爽!

上面还只是转化了一个,让我们来看看更多转化Err的例子

rust 复制代码
use std::fs::File;
use std::io::{self, Read};
use std::num;

#[derive(Debug)]
struct CustomError {
    kind: String,
    message: String,
}

impl From<io::Error> for CustomError {
    fn from(error: io::Error) -> Self {
        CustomError {
            kind: String::from("io"),
            message: error.to_string(),
        }
    }
}

impl From<num::ParseIntError> for CustomError {
    fn from(error: num::ParseIntError) -> Self {
        CustomError {
            kind: String::from("parse"),
            message: error.to_string(),
        }
    }
}

fn main() -> Result<(), CustomError> {
    let mut file = File::open("hello_world.txt")?;

    let mut content = String::new();
    file.read_to_string(&mut content)?;

    let _number: usize;
    _number = content.parse()?;

    Ok(())
}

 

通常在程序中,我们会定义各种不同错误,用在函数中,就会出现函数返回的错误也不一样,例如:

rust 复制代码
use std::fs::read_to_string;

fn main() -> Result<(), std::io::Error> {
  let html = render()?;
  println!("{}", html);
  Ok(())
}

fn render() -> Result<String, std::io::Error> {
  let file = std::env::var("MARKDOWN")?;
  let source = read_to_string(file)?;
  Ok(source)
}

上面的代码运行会报错,因为env::var() 返回的是 std::env::VarError,而 read_to_string 返回的是 std::io::Error。但是我们要求返回的是 std::io::Error,这是工作中非常常见的情形,一开始我刚开始学习的时候十分烦躁,在解决错误处理的时候挠了好久的头。很烦。。但是你们不用烦,继续看,很快就能掌握。

怎么解决这种问题呢?为了满足render函数签名中的返回值类型,我们需要将 std::env::VarErro变为符合std::io::Error类型的Err。

有三种途径

  • 使用特征对象 Box<dyn Error>
  • 自定义错误类型
  • 使用 thiserror
  • 使用 anyhow

第一种:使用特征对象 Box<dyn Error>

在前面当我们实现了Display和Debug后,我们就可以将其转化为标准的 Error triat 即可 trait,这个时候呢,我们把返回值中错误的地方改为 实现了Error triat 即可

rust 复制代码
use std::fs::read_to_string;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
  let html = render()?;
  println!("{}", html);
  Ok(())
}

fn render() -> Result<String, Box<dyn Error>> {
  let file = std::env::var("MARKDOWN")?;
  let source = read_to_string(file)?;
  Ok(source)
}

是不是很方便,这种是最简单的方法,虽然在性能上有一丢丢的损失,但是对于新手来说解决糟心的粗无处理问题来说就非常简单了。

但是这种方式必须实现Display和Debug以符合Error triat

但是不太喜欢这一点的是,原本rust中的错误处理就是让你直面每一种错误,使用这种方式有点自废武功的感觉

下面是他的缺点:

动态分发 : 由于 Box<dyn Error> 使用了动态分发,每次方法调用都需要通过一个额外的间接引用(vtable)来进行。这意味着相对于静态分发(例如具体类型或枚举来表示错误),它可能会稍微慢一些。然而,对于许多应用程序,这种差异是微不足道的。

内存开销 : 使用 Box<dyn Error> 需要在堆上为错误值分配内存。这不仅增加了内存使用量,还可能导致额外的分配和释放开销。

类型信息丢失 : 使用 Box<dyn Error> 可能会导致原始错误类型的信息丢失,除非你进行额外的向下转型操作。这可能会使得错误处理和诊断变得更加困难。

代码复杂性 : 虽然使用 Box<dyn Error> 可以简化一些代码,但在某些情况下,它可能会使得错误处理的逻辑更加复杂。

就性能开销而言,具体的影响因应用程序和使用场景而异。对于I/O密集型或网络密集型应用程序,这种开销可能是可以忽略的,因为主要的瓶颈可能在其他地方。但是,在高性能计算或密集型计算的上下文中,这种额外的开销可能更为明显。

第二种:自定义错误类型

这种方式就是将所有的错误类型放到一个枚举里面,然后Result中Err设置为枚举类型

rust 复制代码
use std::fs::read_to_string;

fn main() -> Result<(), MyError> {
  let html = render()?;
  println!("{}", html);
  Ok(())
}

fn render() -> Result<String, MyError> {
  let file = std::env::var("MARKDOWN")?;
  let source = read_to_string(file)?;
  Ok(source)
}

#[derive(Debug)]
enum MyError {
  EnvironmentVariableNotFound,
  IOError(std::io::Error),
}

impl From<std::env::VarError> for MyError {
  fn from(_: std::env::VarError) -> Self {
    Self::EnvironmentVariableNotFound
  }
}

impl From<std::io::Error> for MyError {
  fn from(value: std::io::Error) -> Self {
    Self::IOError(value)
  }
}

impl std::error::Error for MyError {}

impl std::fmt::Display for MyError {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    match self {
      MyError::EnvironmentVariableNotFound => write!(f, "Environment variable not found"),
      MyError::IOError(err) => write!(f, "IO Error: {}", err.to_string()),
    }
  }
}

但是这种方式,,,,太啰嗦了,不如使用上面的方式,毕竟代码可读性还是很重要的

第三种:使用 thiserror

使用thiserrorr库可以帮我们简化上面的第二种方式:

rust 复制代码
use std::fs::read_to_string;

fn main() -> Result<(), MyError> {
  let html = render()?;
  println!("{}", html);
  Ok(())
}

fn render() -> Result<String, MyError> {
  let file = std::env::var("MARKDOWN")?;
  let source = read_to_string(file)?;
  Ok(source)
}

#[derive(thiserror::Error, Debug)]
enum MyError {
  #[error("Environment variable not found")]
  EnvironmentVariableNotFound(#[from] std::env::VarError),
  #[error(transparent)]
  IOError(#[from] std::io::Error),
}

如上所示,只要简单写写注释,就可以实现错误处理了,惊不惊喜?

并且相比第一种使用dyn Error的方式,使用enum会有更小的性能损耗。在代码易读性上面两者区别不是很大。是比较推荐的一种方式。

第四种使用anyhow

anyhow其实是对第一种方式的封装,其实更慢一些。但是他提供了一些与Err相关的宏工具。

anyhow::Error 的内部主要是一个 Box<dyn std::error::Error + Send + Sync + 'static>。这意味着它可以存储任何实现了 ErrorSendSync trait 并且其生命周期为 'static 的类型。通过利用 Rust 的 From trait,anyhow 允许自动从各种错误类型转换到 anyhow::Error

rust 复制代码
use std::fs::read_to_string;

use anyhow::Result;

fn main() -> Result<()> {
    let html = render()?;
    println!("{}", html);
    Ok(())
}

fn render() -> Result<String> {
    let file = std::env::var("MARKDOWN")?;
    let source = read_to_string(file)?;
    Ok(source)
}

thiserror和anyhow可以单独使用,也可以结合使用,thiserror的主要用处是提供宏简化代码编写,anyhow主要是可以自动转换所有实现Error Trait的错误类型。

将上面的知识装入你的脑子

好了现在你已经不再害怕错误处理了吧,就这么多,以上提到的这些方式足已解决你在工作中遇到的所有错误处理相关的问题!from刘金,转载请注明原文链接。感谢!

相关推荐
gb421528732 分钟前
springboot中Jackson库和jsonpath库的区别和联系。
java·spring boot·后端
程序猿进阶33 分钟前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot
m0_748236111 小时前
Calcite Web 项目常见问题解决方案
开发语言·前端·rust
颜淡慕潇1 小时前
【K8S问题系列 |19 】如何解决 Pod 无法挂载 PVC问题
后端·云原生·容器·kubernetes
LCG元8 小时前
【面试问题】JIT 是什么?和 JVM 什么关系?
面试·职场和发展
向前看-8 小时前
验证码机制
前端·后端
超爱吃士力架10 小时前
邀请逻辑
java·linux·后端
AskHarries12 小时前
Spring Cloud OpenFeign快速入门demo
spring boot·后端
GISer_Jing12 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试