41、Error trait

Error reporting(错误报告)

 

在上一个练习中,我们必须解构TitleError变体以提取错误消息并将其传递给panic!宏。

 

这是错误报告的一个(基本)示例:将错误类型转换为可以向用户、开发者等显式的表示形式。

 

对于每个Rust开发人员来说,想出自己的错误报告策略是不切实际的:这既浪费时间,又不能很好地跨项目组合。这就是Rust提供std::error::Error trait的原因。

 

Error trait

 

对于ResultErr变体的类型没有限制,但是使用实现Error trait的类型是一种良好的做法。Error是Rust错误处理的基石。

// Slightly simplified definition of the `Error` trait
// 稍微简化的 Error trait 的定义
pub trait Error: Debug + Display {}

 

From trait这一节里,讲到了指定supertraits:语法。对Error而言,有两个supertraitsDebugDisplay。如果一个类型想要实现Error,它还必须实现DebugDisplay

 

DisplayDebug

 

在上一节中,我们已经学过了Debug trait——它是assert_eq!用来在断言失败时展示所比较的变量的值得trait。

 

从“mechanical(机械)”的角度看,DisplayDebug是完全相同的——他们都编码了如何将类型转换为类似字符串的表示形式:

// `Debug`
pub trait Debug {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>;
}

// `Display`
pub trait Display {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>;
}

 

区别在于它们的用途:Display返回一个面向“end-users(最终用户)”的表示,而Debug提供了一个更适合开发人员的低级表示。

 

这就是为什么Debug可以使用#[derive(Debug)]属性自动实现,而Display需要手动实现。

 

练习

 

很自然,这次的练习肯定要我们去写Error trait。因为Error trait是Debug trait和Display trait的subtrait,所以我们只需要实现Display trait即可(Debug trait的实现使用宏)。

 

题目:

// TODO: Implement `Debug`, `Display` and `Error` for the `TicketNewError` enum.
//  When implementing `Display`, you may want to use the `write!` macro from Rust's standard library.
//  The docs for the `std::fmt` module are a good place to start and look for examples:
//  https://doc.rust-lang.org/std/fmt/index.html#write
// TODO:为 'TicketNewError' 枚举实现 'Debug'、'Display' 和 'Error'。
//  在实现 'Display' 时,您可能希望使用 Rust 标准库中的 'write!' 宏。
//  您可以从 `std::fmt` 模块的文档入手,查找示例:
//  https://doc.rust-lang.org/std/fmt/index.html#write

#[derive(Debug)]
pub enum TicketNewError {
    TitleError(String),
    DescriptionError(String),
}
/* TODO */
}/* TODO */{}

// TODO: `easy_ticket` should panic when the title is invalid, using the error message
//   stored inside the relevant variant of the `TicketNewError` enum.
//   When the description is invalid, instead, it should use a default description:
//   "Description not provided".
// TODO: 当标题无效时,“easy_ticket”应该会崩溃,使用存储在 'TicketNewError' 枚举的相关变体中的错误消息。
//  如果description无效,他应该使用默认description:"Description not provided"。
pub fn easy_ticket(title: String, description: String, status: Status) -> Ticket {
  /* TODO */  }
}

#[derive(Debug, PartialEq, Clone)]
pub struct Ticket {
    title: String,
    description: String,
    status: Status,
}

#[derive(Debug, PartialEq, Clone)]
pub enum Status {
    ToDo,
    InProgress { assigned_to: String },
    Done,
}

impl Ticket {
    pub fn new(
        title: String,
        description: String,
        status: Status,
    ) -> Result<Ticket, TicketNewError> {
        if title.is_empty() {
            return Err(TicketNewError::TitleError(
                "Title cannot be empty".to_string(),
            ));
        }
        if title.len() > 50 {
            return Err(TicketNewError::TitleError(
                "Title cannot be longer than 50 bytes".to_string(),
            ));
        }
        if description.is_empty() {
            return Err(TicketNewError::DescriptionError(
                "Description cannot be empty".to_string(),
            ));
        }
        if description.len() > 500 {
            return Err(TicketNewError::DescriptionError(
                "Description cannot be longer than 500 bytes".to_string(),
            ));
        }

        Ok(Ticket {
            title,
            description,
            status,
        })
    }

    pub fn title(&self) -> &str {
        &self.title
    }

    pub fn description(&self) -> &str {
        &self.description
    }

    pub fn status(&self) -> &Status {
        &self.status
    }
}

 

分析题目,其实要让我们写的代码并不是很多,我们需要完成Display这个trait的实现,和那个easy_ticket方法。题目给了我一个比较关键的点,就是“在实现 'Display' 时,您可能希望使用 Rust 标准库中的 'write!' 宏。”

 

RustRover没有教你怎么写Display trait的实现,他让咱们自己去研究,好吧,也是培养我们独自查看文档的能力,这都是RustRover的良苦用心。

 

write!

write! and writeln! are two macros which are used to emit the format string to a specified stream. This is used to prevent intermediate allocations of format strings and instead directly write the output. Under the hood, this function is actually invoking the write_fmt function defined on the std::io::Write and the std::fmt::Write trait. Example usage is:

write!writeln! 是两个用于向指定的数据流发送格式字符串的宏,这样可以防止格式字符串的中间分配,而是直接输入输出。在后台,函数实际调用了定义在std::io::Writestd::fmt::Write trait上的 write_fmt 函数。示例用法是:

use std::io::Write;
let mut w = Vec::new();
write!(&mut w, "Hello {}!", "world");

这里的代码应该就是向w这个Vec类型发送格式字符串。

 

我一开始搞不太明白,尝试着敲代码,IDE也给代码提示,图示我就写了上去,写出了这个:

// TODO: Implement `Debug`, `Display` and `Error` for the `TicketNewError` enum.
//  When implementing `Display`, you may want to use the `write!` macro from Rust's standard library.
//  The docs for the `std::fmt` module are a good place to start and look for examples:
//  https://doc.rust-lang.org/std/fmt/index.html#write
// TODO:为 'TicketNewError' 枚举实现 'Debug'、'Display' 和 'Error'。
//  在实现 'Display' 时,您可能希望使用 Rust 标准库中的 'write!' 宏。
//  您可以从 `std::fmt` 模块的文档入手,查找示例:
//  https://doc.rust-lang.org/std/fmt/index.html#write

use std::error::Error;
use std::fmt::{Display, Formatter};

#[derive(Debug)]
pub enum TicketNewError {
    TitleError(String),
    DescriptionError(String),
}

impl Display for TicketNewError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        let output=String::new();
        //write!(output, "{}", f).expect("write!宏错误");
        Ok(())
    }
}

// TODO: `easy_ticket` should panic when the title is invalid, using the error message
//   stored inside the relevant variant of the `TicketNewError` enum.
//   When the description is invalid, instead, it should use a default description:
//   "Description not provided".
// TODO: 当标题无效时,“easy_ticket”应该会崩溃,使用存储在 'TicketNewError' 枚举的相关变体中的错误消息。
//  如果description无效,他应该使用默认description:"Description not provided"。
pub fn easy_ticket(title: String, description: String, status: Status) -> Ticket {
    match Ticket::new(title.clone(),description,status.clone()) {
        Ok(ticket) => {ticket},
        Err(error) => {
            match error {
                TicketNewError::TitleError(title) => {
                    panic!("{title}");
                }
                TicketNewError::DescriptionError(description) => {
                    Ticket::new(title, "Description not provided".to_string(), status).unwrap()
                }
            }
        }
    }

}

#[derive(Debug, PartialEq, Clone)]
pub struct Ticket {
    title: String,
    description: String,
    status: Status,
}

#[derive(Debug, PartialEq, Clone)]
pub enum Status {
    ToDo,
    InProgress { assigned_to: String },
    Done,
}

impl Ticket {
    pub fn new(
        title: String,
        description: String,
        status: Status,
    ) -> Result<Ticket, TicketNewError> {
        if title.is_empty() {
            return Err(TicketNewError::TitleError(
                "Title cannot be empty".to_string(),
            ));
        }
        if title.len() > 50 {
            return Err(TicketNewError::TitleError(
                "Title cannot be longer than 50 bytes".to_string(),
            ));
        }
        if description.is_empty() {
            return Err(TicketNewError::DescriptionError(
                "Description cannot be empty".to_string(),
            ));
        }
        if description.len() > 500 {
            return Err(TicketNewError::DescriptionError(
                "Description cannot be longer than 500 bytes".to_string(),
            ));
        }

        Ok(Ticket {
            title,
            description,
            status,
        })
    }

    pub fn title(&self) -> &str {
        &self.title
    }

    pub fn description(&self) -> &str {
        &self.description
    }

    pub fn status(&self) -> &Status {
        &self.status
    }
}

 

但是很显然,这个答案是错误的。对比一下官方的答案:

impl std::fmt::Display for TicketNewError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            TicketNewError::TitleError(msg) => write!(f, "{}", msg),
            TicketNewError::DescriptionError(msg) => write!(f, "{}", msg),
        }
    }
}

impl std::error::Error for TicketNewError {}
pub fn easy_ticket(title: String, description: String, status: Status) -> Ticket {
    match Ticket::new(title.clone(), description, status.clone()) {
        Ok(ticket) => ticket,
        Err(err) => match err {
            TicketNewError::TitleError(_) => panic!("{err}"),
            TicketNewError::DescriptionError(_) => {
                Ticket::new(title, "Description not provided".to_string(), status).unwrap()
            }
        },
    }
}

 

对于easy_ticket的处理,逻辑上没有什么大的问题,和答案写的基本一致,问题全在Display这个trait实现上。

 

fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result

  • 这是 Display trait 的核心方法:fmt

  • 参数:

    • &self:表示当前实例的不可变引用。
    • f: &mut std::fmt::Formatter:是一个「格式化器」,可以把它理解成“写入目标”,通过它把内容写到输出字符串中。
  • 返回值:std::fmt::Result,即 Ok(())Err(...)。如果格式化过程中出错(罕见),就返回 Err

 

match self { ... }

  • 因为 TicketNewError 很可能是一个枚举类型(enum),所以这里用 match 来处理不同变体(variant)。

  • 从模式匹配可以看出,TicketNewError 有两个变体:

    • TicketNewError::TitleError(msg)
    • TicketNewError::DescriptionError(msg)

其中每个都携带一个 msg 字符串(或实现 Display 的类型,如 String&str)。

 

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

  • 使用 write! 宏将 msg 写入格式化器 f 中。

  • {} 表示占位符,会自动调用 msgDisplay 实现来转为字符串。

  • 例如:

    • 如果 msg"Title cannot be empty",那么输出就是 "Title cannot be empty"
  • 不需要手动 .to_string() 或加引号,write! 会处理好。

 

match返回值是什么?

返回值正是 write!(f, "{}", msg) 的返回结果。

因为 write! 是一个宏,它返回 std::fmt::Result,所以整个 match 表达式也返回 std::fmt::Result,这正是 fmt 函数所需的返回类型。

 

这一节的学习就先到这了

阅读剩余
THE END