更好的错误报告
我们只能接受这样一个事实:错误会发生。与许多其他语言不同的是,在使用 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)