T 和 E 是泛型类型参数;第十章会详细介绍泛型。**现在你需要知道的就是 T 代表成功时返回的 Ok 成员中的数据的类型,而 E 代表失败时返回的 Err 成员中的错误的类型。**因为 Result 有这些泛型类型参数,我们可以将 Result 类型和标准库中为其定义的函数用于很多不同的场景,这些情况中需要返回的成功值和失败值可能会各不相同。
调用一个返回 Result 的函数,因为它可能会失败:
rust复制代码
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
}
如何知道 File::open 返回一个 Result 呢?
VS code中配置完Rust环境后,可以看到返回值
可以查看 标准库 API 文档,或者可以直接问编译器!如果给 f 某个我们知道 不是 函数返回值类型的类型注解,接着尝试编译代码,编译器会告诉我们类型不匹配。然后错误信息会告诉我们 f 的类型 应该 是什么。让我们试试!我们知道 File::open 的返回值不是 u32 类型的,所以将 let f 语句改为如下:
rust复制代码
use std::fs::File;
fn main() {
let f : u32 = File::open("hello.txt");
}
结果
这就告诉我们了 File::open 函数的返回值类型是 Result<T, E>。
机智
这个返回值类型说明 File::open 调用可能会成功并返回一个可以进行读写的文件句柄。这个函数也可能会失败:例如,文件可能并不存在,或者可能没有访问文件的权限。File::open 需要一个方式告诉我们是成功还是失败,并同时提供给我们文件句柄或错误信息。而这些信息正是 Result 枚举可以提供的。
当 File::open 成功的情况下,变量 f 的值将会是一个包含文件句柄的 Ok 实例。在失败的情况下,f 的值会是一个包含更多关于出现了何种错误信息的 Err 实例。
match一下
rust复制代码
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => {
panic!("problem opening ths file: {:?}", error);
}
};
}
注意与 Option 枚举一样,Result 枚举和其成员也被导入到了 prelude 中,所以就不需要在 match 分支中的 Ok 和 Err 之前指定 Result::。
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error => panic!("Problem opening the file: {:?}", other_error),
},
};
}
File::open 返回的 Err 成员中的值类型 io::Error,它是一个标准库中提供的结构体。这个结构体有一个返回 io::ErrorKind 值的 kind 方法可供调用。io::ErrorKind 是一个标准库提供的枚举,它的成员对应 io 操作可能导致的不同错误类型。我们感兴趣的成员是 ErrorKind::NotFound,它代表尝试打开的文件并不存在。所以 match 的 f 匹配,不过对于 error.kind() 还有一个内部 match。
我们希望在匹配守卫中检查的条件是 error.kind() 的返回值是 ErrorKind的 NotFound 成员。如果是,则尝试通过 File::create 创建文件。然而因为 File::create 也可能会失败,还需要增加一个内部 match 语句。当文件不能被打开,会打印出一个不同的错误信息。外部 match 的最后一个分支保持不变,这样对任何除了文件不存在的错误会使程序 panic。
失败时panic 的简写:unwrap和expect
match 能够胜任它的工作,不过它可能有点冗长并且不总是能很好的表明其意图。Result<T, E> 类型定义了很多辅助方法来处理各种情况。其中之一叫做 unwrap。如果 Result 值是成员 Ok,unwrap 会返回 Ok 中的值。如果 Result 是成员 Err,unwrap 会为我们调用 panic!。
rust复制代码
use std::fs::File;
fn main() {
let f = File::open("hello.txt").unwrap();
}
use std::io;
use std::io::Read;
use std::fs::File;
fn read_username_from_file() -> Result<String, io::Error> {
// 打开文件
let f = File::open("hello.txt");
// 匹配打开文件的结果
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};
// 字符串可变变量
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
首先让我们看看函数的返回值:Result<String, io::Error>。这意味着函数返回一个 Result<T, E> 类型的值,其中泛型参数 T 的具体类型是 String,而 E 的具体类型是 io::Error。如果这个函数没有出任何错误成功返回,函数的调用者会收到一个包含 String 的 Ok 值 ------ 函数从文件中读取到的用户名。如果函数遇到任何错误,函数的调用者会收到一个 Err 值,它储存了一个包含更多这个问题相关信息的 io::Error 实例。这里选择 io::Error 作为函数的返回值是因为它正好是函数体中那两个可能会失败的操作的错误返回值:File::open 函数和 read_to_string 方法。
函数体以 File::open 函数开头。接着使用 match 处理返回值 Result,类似于之前示例中的 match,唯一的区别是当 Err 时不再调用 panic!,而是提早返回并将 File::open 返回的错误值作为函数的错误返回值传递给调用者。如果 File::open 成功了,我们将文件句柄储存在变量 f 中并继续。
接着我们在变量 s 中创建了一个新 String 并调用文件句柄 f 的 read_to_string 方法来将文件的内容读取到 s 中。read_to_string 方法也返回一个 Result 因为它也可能会失败:哪怕是 File::open 已经成功了。所以我们需要另一个 match 来处理这个 Result:如果 read_to_string 成功了,那么这个函数就成功了,并返回文件中的用户名,它现在位于被封装进 Ok 的 s 中。如果read_to_string 失败了,则像之前处理 File::open 的返回值的 match 那样返回错误值。不过并不需要显式的调用 return,因为这是函数的最后一个表达式。
调用这个函数的代码最终会得到一个包含用户名的 Ok 值,或者一个包含 io::Error 的 Err 值。
use std::io;
use std::io::Read;
use std::fs::File;
fn read_username_from_file() -> Result<String, io::Error> {
// 打开文件
let mut f = File::open("hello.txt")?;
// 字符串可变变量
let mut s = String::new();
f.read_to_string(&mut s);
Ok(s)
}
Result 值之后的 ? 被定义为与之前示例 中定义的处理 Result 值的 match 表达式有着完全相同的工作方式。如果 Result 的值是 Ok,这个表达式将会返回 Ok 中的值而程序将继续执行。如果值是 Err,Err 中的值将作为整个函数的返回值,就好像使用了 return 关键字一样,这样错误值就被传播给了调用者。
? 运算符所使用的错误值被传递给了 from 函数,它定义于标准库的 From trait 中,其用来将错误从一种类型转换为另一种类型。当 ? 运算符调用 from 函数时,收到的错误类型被转换为定义为当前函数返回的错误类型。这在当一个函数返回一个错误类型来代表所有可能失败的方式时很有用,即使其可能会因很多种原因失败。只要每一个错误类型都实现了 from 函数来定义如将其转换为返回的错误类型,? 运算符会自动处理这些转换。
进一步缩短代码
rust复制代码
use std::io;
use std::io::Read;
use std::fs::File;
fn read_username_from_file() -> Result<String, io::Error> {
let mut s = String::new();
// 打开文件
File::open("hello.txt")?.read_to_string(&mut s);
// 字符串可变变量
Ok(s)
}
在 s 中创建新的 String 被放到了函数开头;这一部分没有变化。我们对 File::open("hello.txt")? 的结果直接链式调用了 read_to_string,而不再创建变量 f。仍然需要 read_to_string 调用结尾的 ?,而且当 File::open 和 read_to_string 都成功没有失败时返回包含用户名 s 的 Ok 值。
rust复制代码
use std::io;
use std::io::Read;
use std::fs::File;
fn read_username_from_file() -> Result<String, io::Error> {
fs::read_to_string("hello.txt")
}
? 运算符可被用于返回值类型为 Result 的函数,atch 的 return Err(e) 部分要求返回值类型是 Result,所以函数的返回值必须是 Result 才能与这个 return 相兼容。
看看在 main 函数中使用 ? 运算符会发生什么,如果你还记得的话其返回值类型是():
rust复制代码
use std::fs::File;
fn main() {
let f = File::open("hello.txt")?;
}
语法糖太多了
9.3 panic!还是不panic!
那么,该如何决定何时应该 panic! 以及何时应该返回 Result 呢?如果代码 panic,就没有恢复的可能。你可以选择对任何错误场景都调用 panic!,不管是否有可能恢复,不过这样就是你代替调用者决定了这是不可恢复的。选择返回 Result 值的话,就将选择权交给了调用者,而不是代替他们做出决定。调用者可能会选择以符合他们场景的方式尝试恢复,或者也可能干脆就认为 Err 是不可恢复的,所以他们也可能会调用 panic! 并将可恢复的错误变成了不可恢复的错误。因此返回 Result 是定义可能会失败的函数的一个好的默认选择。
Rust 的错误处理功能被设计为帮助你编写更加健壮的代码。panic! 宏代表一个程序无法处理的状态,并停止执行而不是使用无效或不正确的值继续处理。Rust 类型系统的 Result 枚举代表操作可能会在一种可以恢复的情况下失败。可以使用 Result 来告诉代码调用者他需要处理潜在的成功或失败。在适当的场景使用 panic! 和 Result 将会使你的代码在面对不可避免的错误时显得更加可靠。