我一直在摸索Rust的文档,试图为我自己的教育利益执行一个简单的深奥的例子而不是实用性。在这样做时,我似乎无法理解Rust的错误处理是如何使用的。
我正在使用的编程示例是编写一个在shell中运行命令的函数。从命令的结果我想要检索stdout
(作为String
或&str
)并知道命令是否失败。
std::process::Command
结构给了我想要的方法,但看起来组合它们的唯一方法是克服和笨拙:
use std::process::Command;
use std::string::{String, FromUtf8Error};
use std::io::Error;
enum CmdError {
UtfError(FromUtf8Error),
IoError(Error),
}
// I would really like to use std::error::Error instead of CmdError,
// but the compiler complains about using a trait in this context.
fn run_cmd(cmd: &str) -> Result<String, CmdError> {
let cmd_result = Command::new("sh").arg("-c").arg(cmd).output();
match cmd_result {
Err(e) => {
return Err(CmdError::IoError(e));
}
Ok(v) => {
let out_result = String::from_utf8(v.stdout);
match out_result {
Err(e) => {
return Err(CmdError::UtfError(e));
}
Ok(v) => {
return Ok(v);
}
}
}
}
}
fn main() {
let r = run_cmd("echo 'Hello World!'");
match r {
Err(e) => {
match e {
CmdError::IoError(e) => {
panic!("Failed to run command {:}", e);
}
CmdError::UtfError(e) => {
panic!("Failed to run command {:}", e);
}
}
}
Ok(e) => {
print!("{:}", e);
}
}
}
特别是,run_cmd
中的嵌套匹配块似乎很尴尬,而main
中的嵌套匹配块甚至更糟。
我真正想做的是能够使用比FromUtf8Error
或io::Error
更常见的错误类型,我可以从任何具体类型轻松转换为类型,但它不会出现类型系统以这种方式设计,所以我不得不使用原油CmdError
而不是union type。
我确信有一个更简单的方法来做这个更惯用,但我还没有从我到目前为止阅读的文档中找到它。
任何pointers赞赏。
定义这样的东西目前不是特别好的东西;您需要使用自定义错误类型设置一些内容,但在完成后,事情会轻松得多。
首先,你需要为std::error::Error
(需要CmdError
和std::fmt::Display
)实现std::fmt::Debug
,然后为了try!
可以自动工作,std::convert::From<std::string::FromUtf8Error>
和std::convert::From<std::io::Error>
。以下是这些的实现:
use std::error::Error;
use std::string::FromUtf8Error;
use std::fmt;
use std::io;
#[derive(Debug)]
enum CmdError {
UtfError(FromUtf8Error),
IoError(io::Error),
}
impl From<FromUtf8Error> for CmdError {
fn from(err: FromUtf8Error) -> CmdError {
CmdError::UtfError(err)
}
}
impl From<io::Error> for CmdError {
fn from(err: io::Error) -> CmdError {
CmdError::IoError(err)
}
}
impl Error for CmdError {
fn description(&self) -> &str {
match *self {
CmdError::UtfError(ref err) => err.description(),
CmdError::IoError(ref err) => err.description(),
}
}
fn cause(&self) -> Option<&Error> {
Some(match *self {
CmdError::UtfError(ref err) => err as &Error,
CmdError::IoError(ref err) => err as &Error,
})
}
}
impl fmt::Display for CmdError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
CmdError::UtfError(ref err) => fmt::Display::fmt(err, f),
CmdError::IoError(ref err) => fmt::Display::fmt(err, f),
}
}
}
(description
实现中的Error
方法可能会返回一个不是基于包装错误的字符串,例如“无法运行命令”。如果想要详细信息,它们仍然会在Error.cause()
中存在。)
实施该批次后,事情变得更加容易,因为我们可以使用try!
。 run_cmd
可以写成:
fn run_cmd(cmd: &str) -> Result<String, CmdError> {
let output = try!(Command::new("sh").arg("-c").arg(cmd).output());
Ok(try!(String::from_utf8(output.stdout)))
}
因为try!
使用From
基础设施,所以这更简单;第一行可能返回Err(CmdError::IoError(_))
(Command.output()
返回Result<_, io::Error>
),第二行可能返回Err(CmdError::UtfError(_))
(String::from_utf8(…)
返回Result<_, FromUtf8Error>
)。
你的main
也可以稍微简单一点,如果你不关心特定的错误,err
分支不需要任何进一步的匹配;因为它现在实现fmt::Display
,你可以直接使用它。
顺便说一下,在格式字符串中,{:}
应该写成{}
;如果没有任何东西,:
是多余的。 ({:?}
可以显示Debug
输出,但如果它面向用户,你应该更喜欢使用Display
。)
?
)Rust现在有一个问号运算符,可以轻松地传播错误。你可以在Rust书中非常完整的chapter about recoverable errors中阅读它。
crates.io上有几个板条箱,可以轻松定义自定义错误类型,而无需编写之前必须编写的所有样板文件。声明零开销错误类型的一种非常简单的方法是使用custom_error
包。我是那个箱子的作者。
结合上述两点,问题中给出的示例可以以非常简洁和可读的方式重写:
use std::string::FromUtf8Error;
use std::io;
use std::process::Command;
use custom_error::custom_error;
custom_error! {CmdError
UtfError{source: FromUtf8Error} = "The command returned an invalid string: {}",
IoError{source: io::Error} = "Unable to launch command: {}"
}
fn run_cmd(cmd: &str) -> Result<String, CmdError> {
let out_bytes = Command::new("sh").arg("-c").arg(cmd).output()?.stdout;
let out_string = String::from_utf8(out_bytes)?;
Ok(out_string)
}
fn main() {
match run_cmd("echo 'Hello World'") {
Ok(res) => println!("{}", res),
Err(e) => eprintln!("{}", e)
}
}