基于 Result<_, E1> 和 Result?,validation,input,error-handling,rust,optional"/>

首页 > 解决方案 > 一种优雅的获取结果的方式基于 Result<_, E1> 和 Result?

问题描述

我在 Rust 中编写了一个“程序”,用于方便地从控制台读取整数:

fn read_i32() -> Result<i32, String> {
  let mut input = String::new();
  match std::io::stdin().read_line(&mut input) {
    Ok(_) => match input.trim_end().parse::<i32>() {
      Ok(integer) => Ok(integer),
      Err(_) => Err(String::from("parsing failed"))
    },
    Err(_) => Err(String::from("reading failed"))
  }
}

fn main() {
  println!("{:?}", read_i32());
}

但是,我使用的错误处理显然很差(来自 C++,我习惯了异常),并且String作为Err我的版本Result可能只是一个 hack。我想要

如何实现?Result<i32, ParseIntError>不够通用,因为问题可能在解析之前发生。.map()和其他功能魔法似乎对read_line()通过.s 链(在我的情况下)之后立即启动它没有用。

标签: validationinputerror-handlingrustoptional

解决方案


Rust 中的错误处理目前仍在发展。您可能会对这篇最近的文章https://nick.groenen.me/posts/rust-error-handling/感兴趣,以获取一些一般性的、最新的建议和讨论。

根据您希望保留多少有关错误的结构化信息,有几种可能的方法。在频谱的一端,您可以使用此错误来构建您自己的精确错误类型:

use thiserror::Error;

#[derive(Error, Debug)]
pub enum MyError {
    #[error("Parsing failed")]
    ParseError { source: std::num::ParseIntError },

    #[error("Reading failed")]
    ReadError { source: std::io::Error },
}

fn read_i32() -> Result<i32, MyError> {
    let mut input = String::new();
    match std::io::stdin().read_line(&mut input) {
        Ok(_) => match input.trim_end().parse::<i32>() {
            Ok(integer) => Ok(integer),
            Err(e) => Err(MyError::ParseError { source: e }),
        },
        Err(e) => Err(MyError::ReadError { source: e }),
    }
}

fn main() {
    println!("{:?}", read_i32());
}

再次使用 的另一种方法thiserror是将源错误直接嵌入到您的错误类型中,使用#[from]. 这允许您使用?运算符自动转换为您的错误类型:

use thiserror::Error;

#[derive(Error, Debug)]
pub enum MyError {
    #[error(transparent)]
    IOError(#[from] std::io::Error),

    #[error(transparent)]
    ParseIntError(#[from] std::num::ParseIntError),
}

fn read_i32() -> Result<i32, MyError> {
    let mut input = String::new();
    std::io::stdin().read_line(&mut input)?;
    let x = input.trim_end().parse::<i32>()?;
    Ok(x)
}

fn main() {
    println!("{:?}", read_i32());
}

这使得更容易产生MyErrors。这里的缺点是,通过这种方式,您放弃了一些将上下文信息添加到错误类型的能力;例如,如果您的函数中有多个位置io::Error可能会发生错误,那么使用第一种方法时,您的错误类型可能包含多个变体以准确识别它发生的位置,以及您可能想要添加的其他上下文信息(例如发生错误的文件中的行号);只是通过底层就失去了这种能力io::Error

另一方面,如果您知道使用的代码read_i32不需要任何有关错误类型的结构化信息,并且您只需要生成人类可读的错误消息,那么就不需要定义自定义错误类型,你可以像这样使用anyhow crate,例如:

use anyhow::{Context, Result};

fn read_i32() -> Result<i32> {
    let mut input = String::new();
    std::io::stdin().read_line(&mut input).context("Read failed")?;
    let x = input.trim_end().parse::<i32>().context("Parse failed")?;
    Ok(x)
}

fn main() {
    println!("{:?}", read_i32());
}

另一方面,如果您甚至不需要那些人类可读的消息(“读取失败”、“解析失败”),那么您也可以将所有错误转换为Box<dyn Error>

use std::error::Error;

fn read_i32() -> Result<i32, Box<dyn Error>> {
    let mut input = String::new();
    std::io::stdin().read_line(&mut input)?;
    let x = input.trim_end().parse::<i32>()?;
    Ok(x)
}

fn main() {
    println!("{:?}", read_i32());
}

但是,随着程序的增长,这可能会使错误更难解释,因为这样它们就不会提供太多上下文。现在有点不方便的一件事是,从 Rust 错误中获取回溯并不容易(至少在稳定的 Rust 中不是这样)。这是目前正在处理的事情(https://github.com/rust-lang/rust/issues/53487)。


推荐阅读