40、Error enums(错误枚举)
Error enums(错误枚举)
神人RustRover,你自己也知道你的代码逆天。上一节的代码给我整不会了,你怎么直接用字符串硬编码匹配错误呢?
不过RustRover自己也知道上一节代码不行,这一节就要做出一些改善了。
在团队协作时,同事可能会重新处理Ticket::new
返回的错误信息(比如为了提高可读性),那么我的代码肯有可能就会发生错误。
解决这个问题的方法很简单——就是枚举。
对错误作出反应
如果要允许调用者根据发生的特定错误才需不同的行为,可以使用枚举来表示不同的错误类型。
// An error enum to represent the different error cases
// that may occur when parsing a `u32` from a string.
enum U32ParseError {
NotANumber,
TooLarge,
Negative,
}
使用错误枚举,就等于在对类型系统中不同错误的情况进行编码,是他们成为可修复错误函数签名的一部分。
这简化了调用者的错误处理,因为他们可以使用match
表达式,来应对不用的错误情况。
match s.parse_u32() {
Ok(n) => n,
Err(U32ParseError::Negative) => 0,
Err(U32ParseError::TooLarge) => u32::MAX,
Err(U32ParseError::NotANumber) => {
panic!("Not a number: {}", s);
}
}
练习
这一节的练习,内容也不少,题目如下:
// TODO: Use two variants, one for a title error and one for a description error.
// Each variant should contain a string with the explanation of what went wrong exactly.
// You'll have to update the implementation of `Ticket::new` as well.
/* TODO */
enum TicketNewError {
/* 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".
pub fn easy_ticket(title: String, description: String, status: Status) -> Ticket {
/* TODO */}
}
#[derive(Debug, PartialEq)]
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(crate) fn new(
title: String,
description: String,
status: Status,
) -> Result<Ticket, TicketNewError> {
if title.is_empty() {
/* TODO */
return Err("Title cannot be empty".to_string());;
}
if title.len() > 50 {
/* TODO */
return Err("Title cannot be longer than 50 bytes".to_string());;
}
if description.is_empty() {
/* TODO */
return Err("Description cannot be empty".to_string());;
}
if description.len() > 500 {
/* TODO */
return Err("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
}
}
我们要完成TicketNewError
枚举类型,easy_ticket
中对错误的匹配和处理,new
方法中对错误信息的返回。
虽然题目说让我们用两个错误变体,但是我写的时候还是尝试用了四个,先把错误枚举填充好,再些new
方法里错误的返回值,最后再完成easy_ticket
方法对错误类型和错误消息进行处理。
代码如下:
// TODO: Use two variants, one for a title error and one for a description error.
// Each variant should contain a string with the explanation of what went wrong exactly.
// You'll have to update the implementation of `Ticket::new` as well.
#[derive(Debug,Clone)]
enum TicketNewError {
TitleNotBeEmpty{message: String},
TitleTooLong{message: String},
DescriptionNotEmpty{message: String},
DescriptionTooLong{message: String},
}
// 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".
pub fn easy_ticket(title: String, description: String, status: Status) -> Ticket {
match Ticket::new(title.clone(),description,status.clone()) {
Err(TicketNewError::TitleNotBeEmpty{message}) => {
panic!("{message}");
}
Err(TicketNewError::TitleTooLong{message}) => {
panic!("{message}");
}
Err(TicketNewError::DescriptionNotEmpty{message}) => {
Ticket::new(title, "Description not provided".to_string(), status).unwrap()
}
Err(TicketNewError::DescriptionTooLong{message}) => {
panic!("{message}");
}
Ok(ticket) => {ticket}
}
}
#[derive(Debug, PartialEq)]
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(crate) fn new(
title: String,
description: String,
status: Status,
) -> Result<Ticket, TicketNewError> {
if title.is_empty() {
return Err(TicketNewError::TitleNotBeEmpty{message: "Title cannot be empty".to_string()});
//return Err("Title cannot be empty".to_string());;
}
if title.len() > 50 {
return Err(TicketNewError::TitleTooLong{message: "Title cannot be longer than 50 bytes".to_string()});
//return Err("Title cannot be longer than 50 bytes".to_string());;
}
if description.is_empty() {
return Err(TicketNewError::DescriptionNotEmpty{message: "Description cannot be empty".to_string()});
//return Err("Description cannot be empty".to_string());;
}
if description.len() > 500 {
return Err(TicketNewError::DescriptionTooLong {message: "Description cannot be longer than 500 bytes".to_string()});
//return Err("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
}
}
但是这样有个很大的问题,原本多种错误发生在一个字段上,硬是把这些错误给拆开,会导致不好的后果。
这道题没有通过,我认真思考了一下原因。Description
其实是可以出现多种错误的,但是这种有的错误能处理,有的错误处理不到的情况,就会导致你只处理了某一个错误,其他的非致命性错误是没有管的。容易产生严重的安全问题,无论怎样都还是存在奇怪的问题。所以还是要按照作者说的那样,使用两个错误变体。
// TODO: Use two variants, one for a title error and one for a description error.
// Each variant should contain a string with the explanation of what went wrong exactly.
// You'll have to update the implementation of `Ticket::new` as well.
#[derive(Debug)]
enum TicketNewError {
TitleError(String),
DescriptionError(String),
}
// 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".
pub fn easy_ticket(title: String, description: String, status: Status) -> Ticket {
match Ticket::new(title.clone(), description, status.clone()) {
Ok(ticket) => ticket,
Err(TicketNewError::DescriptionError(_)) => {
Ticket::new(title, "Description not provided".to_string(), status).unwrap()
}
Err(TicketNewError::TitleError(error)) => panic!("{error}"),
}
}
#[derive(Debug, PartialEq)]
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(crate) 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
}
}
这个代码就很好了。
#[derive(Debug)]
enum TicketNewError {
TitleError(String),
DescriptionError(String),
}
定义了两个枚举变体,分别匹配Title
和Description
的错误。
match Ticket::new(title.clone(), description, status.clone()) {
Ok(ticket) => ticket,
Err(TicketNewError::DescriptionError(_)) => {
Ticket::new(title, "Description not provided".to_string(), status).unwrap()
}
Err(TicketNewError::TitleError(error)) => panic!("{error}"),
}
完全匹配了Result
类型的所有返回值,包括两个错误变体和一个Ok,,匹配DescriptionError
的参数是用_通配符
,这样,就可以统一对是否为空和长度进行处理。
到这我终于明白了,我的代码其实是可以跑的……
只是因为,对DescriptionTooLong
的处理不正确,应该返回一个Ticket::new(title, "Description not provided".to_string(), status).unwrap()
的,但是我却panic了。
果然,修改了这一行代码就没问题了,不过还是,我那个代码确实不好,要避免写这种代码。
pub fn easy_ticket(title: String, description: String, status: Status) -> Ticket {
match Ticket::new(title.clone(),description,status.clone()) {
Err(TicketNewError::TitleNotBeEmpty{message}) => {
panic!("{message}");
}
Err(TicketNewError::TitleTooLong{message}) => {
panic!("{message}");
}
Err(TicketNewError::DescriptionNotEmpty{message}) => {
Ticket::new(title, "Description not provided".to_string(), status).unwrap()
}
Err(TicketNewError::DescriptionTooLong{message}) => {
Ticket::new(title, "Description not provided".to_string(), status).unwrap()
}
Ok(ticket) => {ticket}
}
}
这一节的学习就先到这了。