在error中添加位置信息
2023-12-16

[HowTo] 在 error 中添加位置信息

在 rust 里可以使用 ? 提早返回, 比如下面下面这段代码, 如果 name 中 age 有任意一个为空, 就提前返回.

fn print_info(name: Option<String>, age: Option<u8>) -> Result<(), ()> {
    let name = name.ok_or(())?;
    let age = age.ok_or(())?;
    println!("name: {} age: {}", name, age);
    Ok(())
}

但当我们要确定 print_info 是从哪里返回时, 只能阅读代码, 并确认 name 和 age 哪个为空, 如果函数行数很多或逻辑复杂, 甚至无法确定 name 和 age 值时, 这种方法不是很有用.

一种解决办法是在错误类型里添加位置信息, 在每次调用 ? 时获取调用者, 即外层函数调用? 那一行的位置信息.

rust 有 line!, column!file! 宏可以获取宏调用时的位置信息, 但如果我们这样写永远只能取到 From trait 实现那行的信息

#[derive(Debug, Clone, Copy)]
pub struct LocatedError<T> {
    pub file: &'static str,
    pub line: u32,
    pub column: u32,
    pub error: T,
}

impl<T> From<T> for LocatedError<T> {
    fn from(value: T) -> Self {
        Self {
            file: file!(),
            line: line!(),
            column: column!(),
            error: value,
        }
    }
}

fn print_info_located(name: Option<String>, age: Option<u8>) -> Result<(), LocatedError<()>> {
    let name = name.ok_or(())?;
    let age = age.ok_or(())?;
    println!("name: {} age: {}", name, age);
    Ok(())
}

dbg!(print_info_located(None, None)).ok();
dbg!(print_info_located(Some("PrivateRookie".into()), None)).ok();
dbg!(print_info_located(Some("PrivateRookie".into()), Some(15))).ok();

注意看第一和第二个调用case的返回的错误中位置信息并没有改变

[examples/r.rs:3] print_info_located(None, None) = Err(
    LocatedError {
        file: "examples/r.rs",
        line: 57,
        column: 21,
        error: (),
    },
)
[examples/r.rs:4] print_info_located(Some("PrivateRookie".into()), None) = Err(
    LocatedError {
        file: "examples/r.rs",
        line: 57,
        column: 21,
        error: (),
    },
)
name: PrivateRookie age: 15
[examples/r.rs:5] print_info_located(Some("PrivateRookie".into()), Some(15)) = Ok(
    (),

显然这并不符合我们要求.

在 rust 文档里仔细翻找和搜索后, 找到stack overflow上的一个答案. 使用 track_caller(很遗憾rust文档中没有这个宏的文档)和 std::panic::Location 可以实现上面的需求.

#[derive(Debug, Clone, Copy)]
pub struct LocatedError<T> {
    pub location: Location<'static>,
    pub error: T,
}

impl<T> From<T> for LocatedError<T> {
    #[track_caller] // 注意这里添加了 track_caller 宏
    fn from(value: T) -> Self {
        Self {
            location: *Location::caller(),
            error: value,
        }
    }
}

fn print_info(name: Option<String>, age: Option<u8>) -> Result<(), LocatedError<()>> {
    let name = name.ok_or(())?;
    let age = age.ok_or(())?;
    println!("name: {} age: {}", name, age);
    Ok(())
}

这里最大改变在于给 from 添加了 track_caller, 使用之前的测试代码可以发现错误里携带的正是 print_info ? 对应的行和列.