更好的错误报告
我们只能接受这样一个事实:错误会发生。与许多其他语言不同的是,在使用 rust 时,很难不注意和不处理这个事实:因为无一例外,所有可能的错误状态,通常都编码在函数的返回类型中。
Result
像read_to_string这样的函数,是不返回字符串的。相反,它返回Result,其中(一个是)包含String或(另一种是)某类型的错误(在本例子是std::io::Error)
你怎么知道它是什么?因为啊,Result(其实)是一个enum(枚举),您可以使用match,检查它是哪种变体:
# #![allow(unused_variables)] #fn main() { let result = std::fs::read_to_string("test.txt"); match result { Ok(content) => { println!("File content: {}", content); } Err(error) => { println!("Oh noes: {}", error); } } #}
展开(Unwrap)
现在,我们可以访问文件的内容,但在match区块之后我们不能肯定(它的返回类型)。为此,我们需要以某种方式处理错误案例。这里的挑战在于match块的所有条件语句(或是臂),需要返回相同类型的内容。但有一个巧妙的方法可以解决这个问题:
译:Rust 常把 match 的条件语句,说成 手臂(arm),看起来还挺像的。
# #![allow(unused_variables)] #fn main() { let result = std::fs::read_to_string("test.txt"); let content = match result { Ok(content) => { content }, Err(error) => { panic!("Can't deal with {}, just exit here", error); } }; println!("file content: {}", content); // 使用 content #}
我们可以在match区块之后,使用content的字符串(String 类型)。如果result是错误(Err),则字符串将不存在。但因为程序在到达使用content点之前,就退出了,所以不会有问题。
这可能看起来很刚烈,但很方便。如果您的程序需要读取该文件,并且如果该文件不存在,无法执行任何操作,那么退出是一种有效的策略。甚至还有一个针对Result的快捷方式,就是调用unwrap:
# #![allow(unused_variables)] #fn main() { let content = std::fs::read_to_string("test.txt").unwrap(); #}
不必惊慌
当然,中止/崩溃程序并不是处理错误的唯一方法。不用panic!,我们也可以简单使用return:
# fn main() -> Result<(), Box<std::error::Error>> { let result = std::fs::read_to_string("test.txt"); let _content = match result { Ok(content) => { content }, Err(error) => { return Err(error.into()); } }; # Ok(()) # }
但是,这会改变函数所需的返回类型。实际上,在我们的示例中一直隐藏着一些东西:这个代码所在的函数签名。而在上一个return例子中,它变得很重要。这是完整例子:
fn main() -> Result<(), Box<dyn std::error::Error>> { let result = std::fs::read_to_string("test.txt"); let content = match result { Ok(content) => { content }, Err(error) => { return Err(error.into()); } }; println!("file content: {}", content); Ok(()) }
我们的返回类型是一个Result!这就是为什么我们可以在第二个 match 臂上,写return Err(error);。看看为啥有个Ok(())在底部?因它是函数的默认返回值,表示“结果(Result)正常Ok,且没有内容()”。
问号?
就像match中,可在错误臂调用.unwrap(),作为panic!一样,我们的match有另一个能在错误臂中return的,就是?。
没错,?。可以将此运算符附加到类型Result的值上,Rust 会在内部扩展为类似我们刚刚编写的match语句。
试一试:
fn main() -> Result<(), Box<dyn std::error::Error>> { let content = std::fs::read_to_string("test.txt")?; println!("file content: {}", content); Ok(()) }
非常简洁!
提供上下文
在你的main函数使用?时出现错误,是可以的,但不太好。例如:当你运行std::fs::read_to_string("test.txt")?,但是文件test.txt不存在,您将得到以下输出:
Error: Os { code: 2, kind: NotFound, message: “No such file or directory” }
如果代码中没有包含文件名,就很难判断哪个文件是NotFound。有多种方法可以解决这个问题。
例如,我们可以创建自己的错误类型,然后使用它来构建自定义错误消息:
#[derive(Debug)]
struct CustomError(String);
fn main() -> Result<(), CustomError> {
let path = "test.txt";
let content = std::fs::read_to_string(path)
.map_err(|err| CustomError(format!("Error reading `{}`: {}", path, err)))?;
println!("file content: {}", content);
Ok(())
}
现在,运行此命令,我们将收到自定义错误消息:
Error: CustomError(”Error reading
test.txt: No such file or directory (os error 2)”)
不是说很漂亮,但稍后我们可以为我们的类型,简单调整调试输出。
这种模式实际上很常见。但它有一个问题:我们不存储原始错误,只存储它的字符串表示。常用的failure箱子有一个很好的解决方案:类似于我们的CustomError类型,但它有一个Context会包含说明和原始错误的类型。箱子也带来了一个扩展 trait ResultExt),可以为Result加上context()和with_context()方法。
为了将这些打包的错误类型,转换为人类真正想要读取的内容,我们可以进一步添加exitfailure箱子,并使用其类型作为我们main函数的返回类型。
让我们先导入这些箱子,也就是在Cargo.toml文件的[dependencies]部分,添加failure = "0.1.5"和exitfailure = "0.5.1"。
完整的示例如下:
use failure::ResultExt;
use exitfailure::ExitFailure;
fn main() -> Result<(), ExitFailure> {
let path = "test.txt";
let content = std::fs::read_to_string(path)
.with_context(|_| format!("could not read file `{}`", path))?;
println!("file content: {}", content);
Ok(())
}
这将打印一个错误:
Error: could not read file
test.txt
Info: caused by No such file or directory (os error 2)