Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rust的错误处理 #48

Open
BruceChen7 opened this issue Mar 8, 2023 · 0 comments
Open

rust的错误处理 #48

BruceChen7 opened this issue Mar 8, 2023 · 0 comments

Comments

@BruceChen7
Copy link
Owner

BruceChen7 commented Mar 8, 2023

参考资料

Error trait

  • (https://www.lurklurk.org/effective-rust/errors.html)
  • 函数遇到的所有不同错误都属于同一类型,那么它能直接返回该类型的错误。
  • 出现不同类型的错误时,需要决定是否保留子错误类型信息。
  • Result 的 E 类型参数不一定是实现了 Error 的类型,其是一个 std::error::Error
  • 但 E 这是一个 trait
  • 任何实现 Error 的类型都必须同时实现这两个特性
    • Display trait,能{}和 format! 一起使用
    • Debug trait,能使用 format! 和{:?}一起使用

例子

#[derive(Debug)]
pub struct MyError(String);

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

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

pub fn find_user(username: &str) -> Result<UserId, MyError> {
    let f = std::fs::File::open("/etc/passwd").map_err(|e| {
        MyError(format!("Failed to open password file: {:?}", e))
    })?;
    // ...
}
  • 实现 From trait 会让字符串值很容易转换成 MyError 实例
  • 遇到问号操作符(?)时,编译器会自动应用任何相关的 From trait 实现,以达到错误返回类型的目的
  • 就能进一步减少书写
    pub fn find_user(username: &str) -> Result<UserId, MyError> {
        let f = std::fs::File::open("/etc/passwd")
            .map_err(|e| format!("Failed to open password file: {:?}", e))?;
        // ...
    }
  • 之前的写法是 MyError(xxxx)
  • File::open 返回 std::io::Error 类型的错误。
  • format! 将其转换为 String,通过使用 Debug 实现 std::io::Error 来返回 String
  • ? 使编译器寻找并使用一个 From 实现,它能将编译器从 String 带到 MyError

nested errors

#[derive(Debug)]
pub enum MyError {
    Io(std::io::Error),
    Utf8(std::string::FromUtf8Error),
    General(String),
}

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            MyError::Io(e) => write!(f, "IO error: {}", e),
            MyError::Utf8(e) => write!(f, "UTF-8 error: {}", e),
            MyError::General(s) => write!(f, "General error: {}", s),
        }
    }
}

use std::error::Error;

impl Error for MyError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            MyError::Io(e) => Some(e),
            MyError::Utf8(e) => Some(e),
            MyError::General(_) => None,
        }
    }
}
  • 覆盖默认的 source() 实现有意义,能方便地访问嵌套错误
  • 为所有子错误类型实现 From 特性也是一个好主意
    impl From<std::io::Error> for MyError {
        fn from(e: std::io::Error) -> Self {
            Self::Io(e)
        }
    }
    impl From<std::string::FromUtf8Error> for MyError {
        fn from(e: std::string::FromUtf8Error) -> Self {
            Self::Utf8(e)
        }
    }
  • 使用
    /// Return the first line of the given file.
    pub fn first_line(filename: &str) -> Result<String, MyError> {
        let file = std::fs::File::open(filename)?; // via `From<std::io::Error>`
        let mut reader = std::io::BufReader::new(file);
        let mut buf = vec![];
        let len = reader.read_until(b'\n', &mut buf)?; // via `From<std::io::Error>`
        let result = String::from_utf8(buf)?; // via `From<std::string::FromUtf8Error>`
        if result.len() > MAX_LEN {
            return Err(MyError::General(format!("Line too long: {}", len)));
        }
        Ok(result)
    }
  • 能考虑使用 thiserror crate 来帮助完成一些样板代码,因为它能在不增加额外运行时依赖的情况下减少工作量

panic

  • https://www.shuttle.rs/blog/2022/06/30/error-handling
  • 会停止当前的线程的所有执行,在 rust 中,panic 无法恢复
  • 无法处理的错误,直接使用 panic! 宏
    fn main() {
        // some code
    
        // if we need to debug in here
        panic!();
        // or  panic!("xxx is xx ")
    }
    
    // -------------- Compile time error --------------
    thread 'main' panicked at 'explicit panic', src/main.rs:5:5
  • todo!() 、unimplemented!() 、unreachable!() 都是 panic! () 的包装器,
  • 它们都是根据具体情况专门设计的。

unreachable or unimplemented

fn main() {
    let level = 22;
    let stage = match level {
        1...5 => "beginner",
        6...10 => "intermediate",
        11...20 => "expert",
        _ => unreachable!(),
    };

    println!("{}", stage);
}
// -------------- Compile time error --------------
thread 'main' panicked at 'internal error: entered unreachable code', src/main.rs:7:20

透出错误

use std::collections::HashMap;

fn main() {
  match get_current_date() {
    Ok(date) => println!("We've time travelled to {}!!", date),
    Err(e) => eprintln!("Oh noes, we don't know which era we're in! :( \n  {}", e),
  }
}

fn get_current_date() -> Result<String, reqwest::Error> {
  let url = "https://postman-echo.com/time/object";
  let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;
  let date = res["years"].to_string();

  Ok(date)
}
  • ? 运算符与 unwrap 类似,但它不会 panic,而是将错误传播给调用函数
  • 只能对 Result 或者 Option 使用?运算符

透出多个错误

  use chrono::NaiveDate;
  use std::collections::HashMap;

  fn main() {
    match get_current_date() {
      Ok(date) => println!("We've time travelled to {}!!", date),
      Err(e) => eprintln!("Oh noes, we don't know which era we're in! :( \n  {}", e),
    }
  }

- fn get_current_date() -> Result<String, reqwest::Error> {
+ fn get_current_date() -> Result<String, Box<dyn std::error::Error>> {
    let url = "https://postman-echo.com/time/object";
    let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;

    let formatted_date = format!("{}-{}-{}", res["years"], res["months"] + 1, res["date"]);
    let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")?;
    let date = parsed_date.format("%Y %B %d").to_string();

    Ok(date)
  }
  • 要返回多个错误时,返回一个 trait 对象 Box<dyn std::error::Error> 非常方便
  • 如果不想要客户端 API 处理,这种错误处理比较粗暴。
  • 使用这种 API 方式,使用?来直接返回,这对于不同的分支返回不同的错误糅合到一起,比较简单
  • 注意 Box<dyn Error + Send + Sync + 'static> 一般要绑定这些限制,要加'static 呢?才有能力使用 downcast_ref 的方法
  • 写应用程序,这种定义 API 方式是比较好的,处理错误会简单很多

应用程序和库

  • 返回 Box<dyn std::error::Error> 时,具体类型信息将被删除。
  • 以不同的方式处理不同的错误,需要将它们向下转换为具体类型,而这种转换可能会在运行时失败

error handling for iteration

fn main() {
    let a = ["1", "2", "not a number"]
        .into_iter()
        .map(|s| s.parse::<f64>())
        .collect::<Result<Vec<f64>, _>>();
    dbg!(a);
}
  • a = Err( ParseFloatError { kind: Invalid, }, )

创建自定义错误

  • 库代码,能将所有错误转换为自己的自定义错误并传播它们而不是Box<Error>
  • 目前有两个错误
    • reqwest::Error 
    • chrono::format::ParseError 。
  • 能将它们分别转换为 MyCustomError::HttpError 和 MyCustomError::PkarseError
    pub enum MyCustomError {
      HttpError,
      ParseError,
    }
    impl std::error::Error for MyCustomError {}
    
    impl fmt::Display for MyCustomError {
      fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
          MyCustomError::HttpError => write!(f, "HTTP Error"),
          MyCustomError::ParseError => write!(f, "Parse Error"),
        }
      }
    }
  • 使用 map_err 来转换错误
    // main.rs
    
    + mod error;
    
      use chrono::NaiveDate;
    + use error::MyCustomError;
      use std::collections::HashMap;
    
      fn main() {
        // skipped, will get back later
      }
    
    - fn get_current_date() -> Result<String, Box<dyn std::error::Error>> {
    + fn get_current_date() -> Result<String, MyCustomError> {
        let url = "https://postman-echo.com/time/object";
    -   let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;
    +   let res = reqwest::blocking::get(url)
    +     .map_err(|_| MyCustomError::HttpError)?
    +     .json::<HashMap<String, i32>>()
    +     .map_err(|_| MyCustomError::HttpError)?;
    
        let formatted_date = format!("{}-{}-{}", res["years"], res["months"] + 1, res["date"]);
    -   let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")?;
    +   let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")
    +     .map_err(|_| MyCustomError::ParseError)?;
        let date = parsed_date.format("%Y %B %d").to_string();
    
        Ok(date)
      }
  • 如果实现 From trait,能使用?来进行错误转换
     // error.rs
      use std::fmt;
      #[derive(Debug)]
      pub enum MyCustomError {
        HttpError,
        ParseError,
      }
    
      impl std::error::Error for MyCustomError {}
    
      impl fmt::Display for MyCustomError {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
          match self {
            MyCustomError::HttpError => write!(f, "HTTP Error"),
            MyCustomError::ParseError => write!(f, "Parse Error"),
          }
        }
      }
    
    + impl From<reqwest::Error> for MyCustomError {
    +   fn from(_: reqwest::Error) -> Self {
    +     MyCustomError::HttpError
    +   }
    + }
    
    + impl From<chrono::format::ParseError> for MyCustomError {
    +   fn from(_: chrono::format::ParseError) -> Self {
    +     MyCustomError::ParseError
    +   }
    + }
    
    
      // main.rs
      mod error;
      use chrono::NaiveDate;
      use error::MyCustomError;
      use std::collections::HashMap;
    
      fn main() {
        // skipped, will get back later
      }
    
      fn get_current_date() -> Result<String, MyCustomError> {
        let url = "https://postman-echo.com/time/object";
    -   let res = reqwest::blocking::get(url)
    -     .map_err(|_| MyCustomError::HttpError)?
    -     .json::<HashMap<String, i32>>()
    -     .map_err(|_| MyCustomError::HttpError)?;
    +   let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;
    
        let formatted_date = format!("{}-{}-{}", res["years"], res["months"] + 1, res["date"]);
    -   let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")
    -     .map_err(|_| MyCustomError::ParseError)?;
    +   let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")?;
        let date = parsed_date.format("%Y %B %d").to_string();
    
        Ok(date)
      }
  • 这里实现了 Display 和 Debug trait,更好的还要实现 Send trait

Don't panic

宁愿返回 Result 也不愿使用 panic

fn divide(a: i64, b: i64) -> i64 {
    if b == 0 {
        panic!("Cowardly refusing to divide by zero!");
    }
    a / b
}
fn divide_recover(a: i64, b: i64, default: i64) -> i64 {
    let result = std::panic::catch_unwind(|| divide(a, b));
    match result {
        Ok(x) => x,
        Err(_) => default,
    }
}

let result = divide_recover(0, 0, 42);
println!("result = {}", result); // result = 42
  • 对数据结构进行操作的中途发生了异常,那么就无法保证数据结构处于自洽状态
  • 存在 exception 的情况下维护内部不变性是一件极其困难的事情,
  • panic 传播与 FFI 边界的交互也很差;
  • 对于库代码来说,最好的替代方法是通过返回一个带有适当错误类型的 Result 来使错误成为别人的问题
  • 库 user 自己决定下一步该怎么做,这包括通过 ? 操作符将问题传递给下一个调用者
  • 一个经验法则是,如果你已经控制了 main,那么 panic! (或 unwrap() 、expect() 等)就没问题了
  • 此时,已经没有其他调用者能将责任推给他了
  • 在库代码中,panic! 的另一个合理用法是在极少出错的情况下使用,不希望用户的代码中出现大量的 .unwrap() 调用
  • 如果发生错误的原因仅仅是**(例如)内部数据损坏,而不是输入无效,那么触发 panic! 是合法的**。
  • 允许由无效输入触发的恐慌也是有用的,但这种无效输入并不常见
  • 相关入口点成对出现时,这种方法最有效
    • 一个无懈可击 的版本,其签名意味着它总是成功的(如果不能成功,它就会惊慌失措)、
    • 一个不可靠 的版本,返回一个 Result。
  • 第一个版本,Rust 的 API 指南建议 panic! 应记录在内联文档的特定部分
  • 标准库中的 / String::from_utf8_unchecked 入口点就是第二个的版本
  • panic 不同的形式出现;避免 panic! 也涉及到避免:
    • unwrap() and unwrap_err()
    • expect() and expect_err()
    • unreachable!()
  • 考虑对 Result<T, E>和进行别名处理
    pub type Result<T> = std::result::Result<T, io::Error>;

一些帮助处理错误的 crate

thiserror

use thiserror::Error;

#[derive(Error, Debug)]
pub enum DataStoreError {
    #[error("data store disconnected")]
    Disconnect(#[from] io::Error),
    #[error("the data for key `{0}` is not available")]
    Redaction(String),
    #[error("invalid header (expected {expected:?}, found {found:?})")]
    InvalidHeader {
        expected: String,
        found: String,
    },
    #[error("unknown data store error")]
    Unknown,
}
  • thiserror 提供了一个派生实现,添加了 Error trait。
  • 要实现 Error,必须实现 display,而 thiserrors 的 #[error] 属性为显示的错误提供了模板。

anyhow

use anyhow::{bail, Result, Context};

fn main() -> Result<()> {
    println!("Hello World!");
    func1().context("while calling func1")?;
    Ok(())
}

fn func1() -> Result<()> {
    func2().context("while calling func2")
}

fn func2() -> Result<()> {
    bail!("Hmm something went wrong ")
}

Error: while calling func1

Caused by:
    0: while calling func2
    1: Hmm something went wrong
  • anyhow 为显式处理错误提供了一种符合工程学的惯用替代方法
  • 它与前面提到的错误 trait 类似,但具有额外的功能,例如为抛出的错误添加上下文。
  • 类似于 golang 的错误处理

eyre

  • eyre 是 anyhow 的 fork,增加了更多的回溯信息,高度自定义,帮助你添加颜色等

#type/rust #public

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant