41、Error trait
Error reporting(错误报告)
在上一个练习中,我们必须解构TitleError
变体以提取错误消息并将其传递给panic!
宏。
这是错误报告的一个(基本)示例:将错误类型转换为可以向用户、开发者等显式的表示形式。
对于每个Rust开发人员来说,想出自己的错误报告策略是不切实际的:这既浪费时间,又不能很好地跨项目组合。这就是Rust提供std::error::Error
trait的原因。
Error
trait
对于Result
中Err
变体的类型没有限制,但是使用实现Error
trait的类型是一种良好的做法。Error
是Rust错误处理的基石。
// Slightly simplified definition of the `Error` trait
// 稍微简化的 Error trait 的定义
pub trait Error: Debug + Display {}
在From trait这一节里,讲到了指定supertraits
的:
语法。对Error
而言,有两个supertraits
:Debug
和Display
。如果一个类型想要实现Error
,它还必须实现Debug
和Display
。
Display
和Debug
在上一节中,我们已经学过了Debug
trait——它是assert_eq!
用来在断言失败时展示所比较的变量的值得trait。
从“mechanical(机械)”的角度看,Display
和Debug
是完全相同的——他们都编码了如何将类型转换为类似字符串的表示形式:
// `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!
andwriteln!
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 thewrite_fmt
function defined on thestd::io::Write
and thestd::fmt::Write
trait. Example usage is:
write!
和writeln!
是两个用于向指定的数据流发送格式字符串的宏,这样可以防止格式字符串的中间分配,而是直接输入输出。在后台,函数实际调用了定义在std::io::Write
和std::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
中。 -
{}
表示占位符,会自动调用msg
的Display
实现来转为字符串。 -
例如:
- 如果
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
函数所需的返回类型。
这一节的学习就先到这了