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),
}

定义了两个枚举变体,分别匹配TitleDescription的错误。

 

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}
    }
}

 

这一节的学习就先到这了。

阅读剩余
THE END