Rust中如何自定义错误

当我们啥也不想干的时候,不如先静下心来,想个主题,动手写点东西

格式化输出

{} + Display

在讲Rust自定义错误处理之前,我们先来了解一下Rust中格式化输出的内容,为后面自定义错误处理作个铺垫。

当我们一般使用println!("{}", peter),println! 宏会调用 Display trait 中的 fmt 方法来获得字符串表示,从而成功打印出 peter。

rust 复制代码
pub trait Display {
    // Required method
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>;
}

但是下面代码中Person结构体没有实现Display这个trait会进行报错

rust 复制代码
struct Person<'a> {
    name: &'a str,
    age: u8
}

fn main() {
    let name = "Peter";
    let age = 27;
    let peter = Person { name, age };

    // 美化打印
    println!("{}", peter);
}

下面我们来实现一下这个Display trait

rust 复制代码
use std::fmt;

struct Person<'a> {
    name: &'a str,
    age: u8
}

impl<'a> fmt::Display for Person<'a> {
    // self是Person结构体的引用
    // f是Formatter结构体的可变引用
    // 当我们通过write!来改变f的时候,也就改变了最后println!宏输出的结果
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}今年{}岁", self.name, self.age)
    }
}

fn main() {
    let name = "Peter";
    let age = 27;
    let peter = Person { name, age };

    println!("{}", peter);
}

{:?} + Debug

当你使用 {:?} 占位符时,会调用 std::fmt::Debug trait 的 fmt 方法,它提供了一种格式化类型以便于调试的方式

你可以通过在类型定义前使用 #[derive(Debug)] 属性来自动为类型派生 Debug trait 的实现。

rust 复制代码
#[derive(Debug)]
struct Person<'a> {
    name: &'a str,
    age: u8,
}

fn main() {
    let person = Person {
        name: "Alice",
        age: 30,
    };
    // 使用 Debug trait 的 fmt 方法来格式化并打印 Person 类型的实例
    println!("{:?}", person);
}

错误处理

我们以最常见的读文件来作为案例

rust 复制代码
fn main() {
    let path = "./src/rust.txt";
    match read_file(path) {
        Ok(file) => println!("{}", file),
        Err(err) => println!("{}", err),
    }
}

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

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

但是这带来了一个问题,如果我们需要多个函数处理的结果,每个函数都要返回一个Result类型,那么我们就要match多次,会造成代码臃肿和嵌套地狱的问题,作为一名优秀的程序员,我们是坚决不会允许这种事情发生的!!!

假如我们有以下场景

  1. 读取一文件
  2. 将文件内容转化为UTF-8格式
  3. UTF-8转化为U32的数字
rust 复制代码
///读取文件内容
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>()
}

那么我们读取一个文件,进行以上操作,代码如下,可以看出代码的可读性会非常的差

rust 复制代码
fn main() {
    let path = "./src/rust.txt";
    match read_file(path) {
        Ok(file) => match to_utf8(file.as_bytes()) {
            Ok(v) => match to_u32(v) {
                Ok(t) => println!("num: {}", t),
                Err(err) => println!("{}", err),
            },
            Err(err) => println!("{}", err),
        },
        Err(err) => println!("{}", err),
    }
}

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

fn to_utf8(v: &[u8]) -> Result<&str, std::str::Utf8Error> {
    std::str::from_utf8(v)
}

fn to_u32(v: &str) -> Result<u32, std::num::ParseIntError> {
    v.parse::<u32>()
}

自定义错误

要想细致了解Rust的错误处理,我们需要了解std::error::Error该trait的内部方法,部分代码如下:

rust 复制代码
pub trait Error: Debug + Display {
    // Provided methods
    fn source(&self) -> Option<&(dyn Error + 'static)> { ... }
    fn description(&self) -> &str { ... }
    fn cause(&self) -> Option<&dyn Error> { ... }
    fn provide<'a>(&'a self, request: &mut Request<'a>) { ... }
}

我们可以看出Error主要实现了DebugDisplay这两个trait,还有以下4个方法

  1. source:当我们自定义的错误类型中包含了子错误,我们可以重写source方法,用来指定当前错误的底层来源
    • 如果当前Error是低级别的Error,并没有子Error ,需要返回None。介于其本身默认有返回值None,可以不覆盖该方法。
    • 如果当前Error包含子Error ,需要返回子Error :Some(err),需要覆盖该方法。
scss 复制代码
pub enum StorageError {
    FileHandle(FileHandleError),
    ParseItem(ParseItemError),
    ItemNoExist(u32),
}

impl Error for StorageError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        use FileHandleError::*;
        use StorageError::*;
        match self {
            FileHandle(source) => match source {
                EnvVar(source) => Some(source),
                Io(source) => Some(source),
            },
            ParseItem(e) => Some(e),
            ItemNoExist(_) => None,
        }
    }
}
  1. description,已经被废弃了,使用display方法替代
  2. cause,也已经被废弃了,从rust1.33版本开始,被Error::source方法替代了
  1. provide:实验性的API,还在开发阶段,我们可以先不管

总结:实现一个自定义错误的步骤如下:

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

自定义CustomError

rust 复制代码
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"),
    }
}

println!("Error: {}", e)时,实际上就调用了CustomErrorDisplay trait中的fmt方法,

println!("Caused by: {}", e.source().unwrap()),实际上先调用CustomError的souce方法,返回了子错误,并且是个Some类型,所以这时候使用unwrap程序就不会崩溃了。最后打印的是子错误上的fmt方法

重写文件处理

最后,我们用自定义错误重写下文件处理,不想写了,代码放下面,大家自己看吧!!!

最最后讲一个点吧,?运算符会尝试从Result中获取值,如果不成功,它就会接受Error,中止函数执行,并把错误传播到调用该函数的函数。同时? 操作符依赖于 From trait。如果错误类型实现了 From trait 用于从原始错误类型转换为目标错误类型,? 操作符就会使用这个实现来自动进行错误转换。

rust 复制代码
use std::fmt::{Display, Formatter};
use std::io::Error as IoError;
use std::num::ParseIntError;
use std::str::Utf8Error;

fn main() -> std::result::Result<(), CustomError> {
    let path = "./src/rust.txt";
    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)
    }
}
相关推荐
小杍随笔4 小时前
【Rust Exercism 练习详解:Anagram + Space Age + Sublist(附完整代码与深度解读)】
开发语言·rust·c#
Rust研习社6 小时前
Rust 字符串与切片实战
rust
朝阳5816 小时前
局域网聊天工具
javascript·rust
朝阳5816 小时前
我做了一个局域网传文件的小工具,记录一下
javascript·rust
Rust语言中文社区20 小时前
【Rust日报】用 Rust 重写的 Turso 是一个更好的 SQLite 吗?
开发语言·数据库·后端·rust·sqlite
小杍随笔1 天前
【Rust 半小时速成(2024 Edition 更新版)】
开发语言·后端·rust
Source.Liu1 天前
【office2pdf】office2pdf 纯 Rust 实现的 Office 转 PDF 库
rust·pdf·office2pdf
洛依尘1 天前
深入浅出 Rust 生命周期:它不是语法负担,而是借用关系的说明书
后端·rust
Rust研习社1 天前
通过示例学习 Rust 模式匹配
rust
PaytonD1 天前
基于 GPUI 实现 WebScoket 服务端之服务篇
后端·rust