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)
    }
}
相关推荐
码农飞飞2 小时前
深入理解Rust的模式匹配
开发语言·后端·rust·模式匹配·解构·结构体和枚举
一个小坑货2 小时前
Rust 的简介
开发语言·后端·rust
qq_172805592 小时前
RUST学习教程-安装教程
开发语言·学习·rust·安装
monkey_meng3 小时前
【遵守孤儿规则的External trait pattern】
开发语言·后端·rust
新知图书4 小时前
Rust编程与项目实战-模块std::thread(之一)
开发语言·后端·rust
fqbqrr8 小时前
2411rust,实现特征
rust
码农飞飞8 小时前
详解Rust枚举类型(enum)的用法
开发语言·rust·match·枚举·匹配·内存安全
一个小坑货14 小时前
Cargo Rust 的包管理器
开发语言·后端·rust
bluebonnet2714 小时前
【Rust练习】22.HashMap
开发语言·后端·rust
VertexGeek16 小时前
Rust学习(八):异常处理和宏编程:
学习·算法·rust