让不可能的状态变得不可能#
在Rust中,没有像undefined 或null ,也没有像你在Java或C#等编程语言中知道的异常。相反,你使用内置的枚举来模拟状态。
Option<T>用于可能没有值的绑定(例如: 或 )Some(x)NoneResult<T, E>用于可能出错的操作结果(例如: 对 )。Ok(val)Err(error)
两者之间的区别是非常细微的,在很大程度上取决于你的代码的语义。不过这两个枚举的工作方式是非常相似的。在我看来,最重要的是这两种类型都要求你去处理它们。要么明确地处理所有状态,要么明确地忽略它们。
在这篇文章中,我想把重点放在Result<T, E> ,因为这其中实际上包含了错误。
Result<T, E> 是一个有两个变体的枚举。
enum Result<T, E> {
Ok(T),
Err(E),
}
T E T 可以是任何值, 可以是任何错误。两个变体 和 是全局可用的。E Ok Err
当你有可能出错的事情时,使用Result<T, E> 。一个预计会成功的操作,但可能会有不成功的情况。一旦你有了一个Result 的值,你就可以做以下事情。
- 处理好这些状态!
- 忽略它
- 惊慌失措!
- 使用回退
- 传播错误
让我们看看我的详细意思。
处理错误状态#
让我们写一个小作品,我们想从一个文件中读取一个字符串。它要求我们
- 读取一个文件
- 从这个文件中读取一个字符串
这两个操作都可能导致std::io::Error ,因为可能会发生一些不可预见的事情(文件不存在,或者不能从文件中读出,等等)。所以我们要写的函数可以返回一个String ,也可以返回一个io::Error 。
use std::io;
use std::fs::File;
fn read_username_from_file(path: &str) -> Result<String, io::Error> {
let f = File::open(path);
/* 1 */
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut s = String::new();
/* 2 */
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(err) => Err(err),
}
}
这就是所发生的事情。
- 当我们从
path中打开一个文件时,它要么返回一个文件柄来与Ok(file),要么导致一个错误Err(e)。通过match f,我们不得不处理这两种可能的状态。要么我们把文件柄分配给f(注意f的阴影),要么我们通过返回错误从该函数中返回。这里的return语句很重要,因为我们要退出该函数。 - 然后我们要把内容读到
s,即我们刚刚创建的字符串中。这同样可以成功或抛出一个错误。函数f.read_to_string返回读取的字节长度,所以我们可以安全地忽略这个值,并返回一个带有读取的字符串的Ok(s)。在其他情况下,我们只是返回同样的错误。注意,我没有在match表达式的末尾写上分号。由于它是一个表达式,这就是我们此时从函数中返回的东西。
这可能看起来非常冗长(确实如此......),但你看到了错误处理的两个非常重要的方面。
- 在这两种情况下,你都要处理这两种可能的状态。如果不做什么,你就不能继续了
- 像影子(将一个值绑定到一个现有的名字上)和表达式这样的特性使得即使是冗长的代码也容易阅读和使用
我们刚才的操作通常被称为解包。因为你把包裹在枚举中的值解开了。
说到解包...
忽略那些错误#
如果你非常确信你的程序不会失败,你可以简单地使用内置函数.unwrap() 你的值。
fn read_username_from_file(path: &str) -> Result<String, io::Error> {
let mut f = File::open(path).unwrap(); /* 1 */
let mut s = String::new();
f.read_to_string(&mut s).unwrap(); /* 1 */
Ok(s) /* 2 */
}
下面是发生的情况。
- 在所有可能导致错误的情况下,我们都会调用
unwrap(),以获得该值 - 我们把结果包在一个
Ok变体中,然后返回。我们可以直接返回s,并在我们的函数签名中删除Result<T, E>。我们保留它是因为我们在其他例子中也会用到它。
unwrap() 函数本身很像我们在第一步中处理所有状态的做法。
// result.rs
impl<T, E: fmt::Debug> Result<T, E> {
// ...
pub fn unwrap(&self) -> T {
match self {
Ok(t) => t,
Err(e) => unwrap_failed("called `Result::unwrap()` on an `Err` value", &e),
}
}
// ...
}
unwrap_failed 是 宏的一个快捷方式。这意味着如果你使用 ,而你没有一个成功的结果,你的软件就会崩溃。😱panic! .unwrap()
你可能会问自己。这与其他编程语言中直接使软件崩溃的错误有什么不同?答案很简单:你必须明确这一点。Rust要求你做一些事情,即使是明确地允许恐慌。
有很多不同的.unwrap_ ,你可以在各种情况下使用这些函数。我们进一步看一下其中的一两个。
恐慌
说到恐慌,你也可以用你自己的恐慌信息进行恐慌。
fn read_username_from_file(path: &str) -> Result<String, io::Error> {
let mut f = File::open(path).expect("Error opening file");
let mut s = String::new();
f.read_to_string(&mut s).unwrap("Error reading file to string");
Ok(s)
}
.expect(...) 所做的事情与之非常相似unwrap()
impl<T, E: fmt::Debug> Result<T, E> {
// ...
pub fn expect(self, msg: &str) -> T {
match self {
Ok(t) => t,
Err(e) => unwrap_failed(msg, &e),
}
}
}
但是,你的恐慌信息在你手中,你可能会喜欢!
但是,即使我们在任何时候都是明确的,我们可能希望我们的软件不要一遇到错误状态就恐慌和崩溃。我们可能想做一些有用的事情,比如提供回退或者......好吧......实际处理错误。
回退值#
Rust有可能在其Result (和Option )枚举上使用默认值。
fn read_username_from_file(path: &str) -> Result<String, io::Error> {
let mut f = File::open(path).expect("Error opening file");
let mut s = String::new();
f.read_to_string(&mut s).unwrap_or("admin"); /* 1 */
Ok(s)
}
"admin"对于一个用户名来说,默认值可能不是最好的回退,但你会明白这个意思。在出现错误结果的情况下,我们不会崩溃,而是返回一个默认值。 方法为更复杂的默认值采取了一个闭包。.unwrap_or_else
这就更好了!不过,到目前为止,我们所学到的是在非常冗长、或允许明确的崩溃、或可能有回退值之间进行权衡。但我们能不能两者兼得?简洁的代码和错误安全?我们可以!
传播错误#
我最喜欢Rust的Result 类型的一个特点是可以传播错误。可能导致错误的两个函数都有相同的错误类型:io::Error 。我们可以在每个操作后使用问号操作符,为快乐的路径编写代码(只有成功的结果),如果出错,则返回错误结果。
fn read_username_from_file(path: &str) -> Result<String, io::Error> {
let mut f = File::open(path)?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
在这块,f 是一个文件处理程序,f.read_to_string 保存到s 。如果出了问题,我们用Err(io::Error) 从函数返回。简洁的代码,但我们处理了上面一层的错误。
fn main() {
match read_username_from_file("user.txt") {
Ok(username) => println!("Welcome {}", username),
Err(err) => eprintln!("Whoopsie! {}", err)
};
}
它的好处是什么?
- 我们仍然是明确的,我们必须要做一些事情!你仍然可以找到所有可能发生错误的地方!
- 我们可以写出简洁的代码,就好像错误不会存在一样。错误还是要处理的!要么来自我们,要么来自我们函数的用户。
问号运算符也可以在Option<T> ,这也可以写出一些非常漂亮和优雅的代码
传播不同的错误#
但问题是,像这样的方法只有在错误类型相同的情况下才起作用。如果我们有两种不同类型的错误,我们就必须要有创造性。看看这个稍作修改的函数,我们打开并读取文件,但随后将读取的内容解析成一个u64
fn read_number_from_file(filename: &str) -> Result<u64, ???> {
let mut file = File::open(filename)?; /* 1 */
let mut buffer = String::new();
file.read_to_string(&mut buffer)?; /* 1 */
let parsed: u64 = buffer.trim().parse()?; /* 2 */
Ok(parsed)
}
- 这两个点可以引起
io::Error,正如我们在前面的例子中知道的那样 - 这个操作却会引起一个
ParseIntError
问题是,我们不知道在编译时得到哪个错误。这完全取决于我们的代码运行。我们可以通过match 表达式来处理每个错误,并返回我们自己的错误类型。这是有道理的,但使我们的代码又变得冗长。或者我们为 "在运行时发生的事情 "做准备!
看看我们稍微改变的函数
use std::error;
fn read_number_from_file(filename: &str) -> Result<u64, Box<dyn error::Error>> {
let mut file = File::open(filename)?; /* 1 */
let mut buffer = String::new();
file.read_to_string(&mut buffer)?; /* 1 */
let parsed: u64 = buffer.trim().parse()?; /* 2 */
Ok(parsed)
}
这就是所发生的事情。
- 我们没有返回一个错误实现,而是告诉Rust,实现
Error错误特性的东西正在出现。 - 由于我们在编译时不知道这可能是什么,我们必须把它变成一个特质对象:
dyn std::error::Error。 - 因为我们不知道这个对象会有多大,所以我们用一个
Box。一个智能指针,指向最终会在堆上的数据。
Box<dyn Trait> 在Rust中实现了动态调度。可以动态地调用一个在编译时不知道的函数。为此,Rust引入了一个vtable,用来保存指向实际实现的指针。在运行时,我们使用这些指针来调用适当的函数实现。
而现在,我们的代码又变得简洁了,而我们的用户则要处理最终的错误。
当我在课程中向人们展示这一点时,我得到的第一个问题是。但我们最终能不能检查出哪种类型的错误?我们可以downcast_ref() 方法允许我们回到原来的类型。
fn main() {
match read_number_from_file("number.txt") {
Ok(v) => println!("Your number is {}", v),
Err(err) => {
if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
eprintln!("Error during IO! {}", io_err)
} else if let Some(pars_err) = err.downcast_ref::<ParseIntError>() {
eprintln!("Error during parsing {}", pars_err)
}
}
};
}
Groovy!
自定义错误#
如果你想为你的操作创建自定义错误,情况会变得更好,更灵活。要使用自定义错误,你的错误结构必须实现std::error::Error 特质。这可以是一个经典结构,一个元组结构,甚至是一个单元结构。
你不需要实现std::error::Error 的任何功能,但是你需要实现Debug 和Display 特质。理由是错误要被打印在某个地方。下面是一个例子的样子。
#[derive(Debug)] /* 1 */
pub struct ParseArgumentsError(String); /* 2 */
impl std::error::Error for ParseArgumentsError {} /* 3 */
/* 4 */
impl Display for ParseArgumentsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
- 我们派生出
Debug特质。 - 我们的
ParseArgumentsError是一个有一个元素的元组结构。一个自定义的消息 - 我们为
ParseArgumentsError实现std::error::Error。不需要实现其他东西 - 我们实现
Display,在这里我们打印出我们元组的单个元素。
就这样吧!