基于 Result<_, E1> 和 Result<T, E2> 获取 Result<T, E> 的优雅方式?
An elegant way of getting Result<T, E> based on Result<_, E1> and Result<T, E2>?
我用 Rust 写了一个 "program" 来方便地从控制台读取整数:
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
作为我 [=15= 的 Err
版本] 可能只是一个黑客。我要
- 读到任何空白字符,不仅
'\n'
;
- 避免在每次操作后进行显式 C 风格的错误检查;
- 为
read_i32()
使用更好的 "error-type"。
如何实现? Result<i32, ParseIntError>
不够通用,因为问题可能发生在解析之前。 .map()
和其他功能魔法对于在 read_line()
之后通过一系列 .
立即启动它似乎没有用(在我的例子中)。
Rust 中的错误处理目前仍在不断发展。您可能会对最近的这篇文章 https://nick.groenen.me/posts/rust-error-handling/ 感兴趣,以获取一些一般的、最新的建议和讨论。
根据您要保留的有关错误的结构化信息量,有几种可能的方法。一方面,您可以使用 thiserror 来构造您自己的精确错误类型:
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());
}
这使得生成 MyError
更容易。这里的缺点是,通过这种方式,您放弃了一些将上下文信息添加到错误类型的能力;例如,如果您的函数中有多个地方可能会出现 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());
}
另一方面,如果您甚至不需要那些人类可读的消息("Read failed"、"Parse failed"),那么您也可以将所有错误转换为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)。
我用 Rust 写了一个 "program" 来方便地从控制台读取整数:
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
作为我 [=15= 的 Err
版本] 可能只是一个黑客。我要
- 读到任何空白字符,不仅
'\n'
; - 避免在每次操作后进行显式 C 风格的错误检查;
- 为
read_i32()
使用更好的 "error-type"。
如何实现? Result<i32, ParseIntError>
不够通用,因为问题可能发生在解析之前。 .map()
和其他功能魔法对于在 read_line()
之后通过一系列 .
立即启动它似乎没有用(在我的例子中)。
Rust 中的错误处理目前仍在不断发展。您可能会对最近的这篇文章 https://nick.groenen.me/posts/rust-error-handling/ 感兴趣,以获取一些一般的、最新的建议和讨论。
根据您要保留的有关错误的结构化信息量,有几种可能的方法。一方面,您可以使用 thiserror 来构造您自己的精确错误类型:
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());
}
这使得生成 MyError
更容易。这里的缺点是,通过这种方式,您放弃了一些将上下文信息添加到错误类型的能力;例如,如果您的函数中有多个地方可能会出现 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());
}
另一方面,如果您甚至不需要那些人类可读的消息("Read failed"、"Parse failed"),那么您也可以将所有错误转换为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)。