46、Error-source

Error::source

 

为了完成对Error trait的学习,我们还需要学习一个东西:source方法。

// Full definition this time!
pub trait Error: Debug + Display {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        None
    }
}

 

source方法是访问错误原因(如果有)的一种方法。错误通常是链式的,这意味着一个错误是另一个错误的原因:我们有一个较低级别的错误(例如无法解析数据库主机名)引起的高级错误(例如无法连接到数据库)。source方法允许我们遍历整个错误链,通常在日志捕获错误上下文的时候使用。

 

实现source

 

Errortrait提供了一个默认实现始终返回None(即没有根本原因)。这就是在前面的练习中我们不必关心source的原因。

 

我们可以覆盖这个默认实现,为我们的错误类型提供原因。

use std::error::Error;

#[derive(Debug)]
struct DatabaseError {
    source: std::io::Error
}

impl std::fmt::Display for DatabaseError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "Failed to connect to the database")
    }
}

impl std::error::Error for DatabaseError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        Some(&self.source)
    }
}

 

在上面的代码中,DatabaseError封装了一个std::io::Error作为source。然后,我们重写source方法,以便在调用时返回source。

 

&(dyn Error + 'static)

 

&(dyn Error + 'static)是什么类型?

 

让我们拆分一下:

  • dyn Error是一个trait对象。这是一种引用实现了Errortrait的任何类型的方式。
  • 'static是一个特殊的生命周期说明符,意味着引用在整个程序运行时有效。

 

组合起来:&(dyn Error + 'static)是对实现了Errortrait,并且对整个程序执行有效的trait对象的引用。

现在还不需要太担心这两个概念,后面还会再学。

 

使用thiserror实现source

 

thiserror提供了三种方式为我们自己的错误类型实现source

  • 名为source的字段将自动用作错误的source。
use thiserror::Error;

#[derive(Error, Debug)]
pub enum MyError {
    #[error("Failed to connect to the database")]
    DatabaseError {
        source: std::io::Error
    }
}
  • 带有#[source]属性注释的字段将自动用作错误的source。
use thiserror::Error;

#[derive(Error, Debug)]
pub enum MyError {
    #[error("Failed to connect to the database")]
    DatabaseError {
        #[source]
        inner: std::io::Error
    }
}
  • 使用#[from]属性注释的字段将自动用作错误的source,并且thiserror将自动生成From实现,以将带注释的类型转换为错误类型。
use thiserror::Error;

#[derive(Error, Debug)]
pub enum MyError {
    #[error("Failed to connect to the database")]
    DatabaseError {
        #[from]
        inner: std::io::Error
    }
}

 

?操作符

?操作符是传播错误的简写。

在返回Result的函数中使用时,如果ResultErr,它将提前带错返回。

 

比如:

use std::fs::File;

fn read_file() -> Result<String, std::io::Error> {
    let mut file = File::open("file.txt")?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

等同于:

use std::fs::File;

fn read_file() -> Result<String, std::io::Error> {
    let mut file = match File::open("file.txt") {
        Ok(file) => file,
        Err(e) => {
            return Err(e);
        }
    };
    let mut contents = String::new();
    match file.read_to_string(&mut contents) {
        Ok(_) => (),
        Err(e) => {
            return Err(e);
        }
    }
    Ok(contents)
}

 

我们可以使用?操作符以大大缩短处理错误的代码。

特别是,如果?可以转换(即如果有合适的From实现),?操作符会自动地把可修复错误类型转换为函数的错误类型。

 

练习

 

题目lib.rs:

pub use crate::status::{ParseStatusError, Status};

// We've seen how to declare modules in one of the earliest exercises, but
// we haven't seen how to extract them into separate files.
// Let's fix that now!
//
// In the simplest case, when the extracted module is a single file, it is enough to
// create a new file with the same name as the module and move the module content there.
// The module file should be placed in the same directory as the file that declares the module.
// In this case, `src/lib.rs`, thus `status.rs` should be placed in the `src` directory.
mod status;

// TODO: Add a new error variant to `TicketNewError` for when the status string is invalid.
//   向“TicketNewError”添加新的错误变体,以便在状态字符串无效时使用。
//   When calling `source` on an error of that variant, it should return a `ParseStatusError` rather than `None`.
//   当对该变体的错误调用“source”时,它应该返回“ParseStatusError”而不是“None”。

#[derive(Debug, thiserror::Error)]
pub enum TicketNewError {
    #[error("Title cannot be empty")]
    TitleCannotBeEmpty,
    #[error("Title cannot be longer than 50 bytes")]
    TitleTooLong,
    #[error("Description cannot be empty")]
    DescriptionCannotBeEmpty,
    #[error("Description cannot be longer than 500 bytes")]
    DescriptionTooLong,
    /* TODO */,
}

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

impl Ticket {
    pub fn new(title: String, description: String, status: String) -> Result<Self, TicketNewError> {
        if title.is_empty() {
            return Err(TicketNewError::TitleCannotBeEmpty);
        }
        if title.len() > 50 {
            return Err(TicketNewError::TitleTooLong);
        }
        if description.is_empty() {
            return Err(TicketNewError::DescriptionCannotBeEmpty);
        }
        if description.len() > 500 {
            return Err(TicketNewError::DescriptionTooLong);
        }

        // TODO: Parse the status string into a `Status` enum.
        //  将状态字符串解析为一个 `Status` 枚举。
       /* TODO */;

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

 

status.rs:

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

impl TryFrom<String> for Status {
    type Error = ParseStatusError;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        let value = value.to_lowercase();
        match value.as_str() {
            "todo" => Ok(Status::ToDo),
            "inprogress" => Ok(Status::InProgress),
            "done" => Ok(Status::Done),
            _ => Err(ParseStatusError {
                invalid_status: value,
            }),
        }
    }
}

#[derive(Debug, thiserror::Error)]
#[error("`{invalid_status}` is not a valid status. Use one of: ToDo, InProgress, Done")]
pub struct ParseStatusError {
    invalid_status: String,
}

 

我们要在TicketNewError中添加新的错误变体,这个错误变体实现source,和status的错误相关联(大概是这么个意思)。我们要修改两处代码。

 

其中一处是错误枚举TicketNewError里面的,添加字段InvalidStatus,然后实现source,对应的是status里面的ParseStatusError

#[derive(Debug, thiserror::Error)]
#[error("`{invalid_status}` is not a valid status. Use one of: ToDo, InProgress, Done")]
pub struct ParseStatusError {
    invalid_status: String,
}

 

ParseStatusError实现了thiserror::Error宏,并指定了错误信息。于是我们可以编写这样的代码:

#[error("{0}")]
InvalidStatus(#[from] ParseStatusError),

 

TicketNewError枚举变成了这样:

#[derive(Debug, thiserror::Error)]
pub enum TicketNewError {
    #[error("Title cannot be empty")]
    TitleCannotBeEmpty,
    #[error("Title cannot be longer than 50 bytes")]
    TitleTooLong,
    #[error("Description cannot be empty")]
    DescriptionCannotBeEmpty,
    #[error("Description cannot be longer than 500 bytes")]
    DescriptionTooLong,
    #[error("{0}")]
    InvalidStatus(#[from] ParseStatusError),
}

 

然后第二处编写代码为:

let status = Status::try_from(status)?;

 

参数status的类型是String类型,这行语句用try_from(带错误处理的from方法)把String转换为了Status里面的枚举。

调用了这里的函数:

impl TryFrom<String> for Status {
    type Error = ParseStatusError;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        let value = value.to_lowercase();
        match value.as_str() {
            "todo" => Ok(Status::ToDo),
            "inprogress" => Ok(Status::InProgress),
            "done" => Ok(Status::Done),
            _ => Err(ParseStatusError {
                invalid_status: value,
            }),
        }
    }
}

 

这段代码其实就只允许String状态的status有三个可用的字段,如果出现预料之外的匹配,那就是返回一个ParseStatusError错误枚举变体,又回到了这个枚举类型的定义:

#[derive(Debug, thiserror::Error)]
#[error("`{invalid_status}` is not a valid status. Use one of: ToDo, InProgress, Done")]
pub struct ParseStatusError {
    invalid_status: String,
}

 

同时这里是携带了一个字符串,字符串的内容就是String状态下的status的不在匹配范围内的值。

 

当发生错误时,Ticket::new就会返回错误,调用这个Ticket::new的东西就可以处理这个错误了。

 

现在再回到上面定义TicketNewError变体的时候:

#[derive(Debug, thiserror::Error)]
pub enum TicketNewError {
    #[error("Title cannot be empty")]
    TitleCannotBeEmpty,
    #[error("Title cannot be longer than 50 bytes")]
    TitleTooLong,
    #[error("Description cannot be empty")]
    DescriptionCannotBeEmpty,
    #[error("Description cannot be longer than 500 bytes")]
    DescriptionTooLong,
    #[error("{0}")]
    InvalidStatus(#[from] ParseStatusError),
}

#[from] ParseStatusError这里的#[from]其实就是指明了这个错误枚举变体的来源是ParseStatusError,是Status里面的ParseStatusError。顺利完成了错误的链式关联。

 

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

阅读剩余
THE END