跳转到内容

Rust Result

Option 表示一个可能缺失的值,而 Result 表示一个可能失败的操作。和“值不存在”不同,失败通常会附带一个原因。

Result 是标准库里的另一个枚举,这次它的两个变体都带着数据:

enum Result<T, E> {
Ok(T), // 成功,装着一个值
Err(E), // 失败,装着一个错误
}

你在类型转换那一页见过的 .parse() 就是一个很好的真实例子:把文本转换成数字,如果文本根本不是数字,就会失败:

fn main() {
let good: Result<i32, std::num::ParseIntError> = "42".parse();
let bad: Result<i32, std::num::ParseIntError> = "abc".parse();
println!("{:?}", good);
println!("{:?}", bad);
}

输出:

Ok(42)
Err(ParseIntError { kind: InvalidDigit })
▶ 动手试一试
fn main() {
let input = "42";
match input.parse::<i32>() {
Ok(number) => println!("解析成功:{number}"),
Err(e) => println!("解析失败:{e}"),
}
}
▶ 动手试一试

写法和用 match 处理 Option 一样,区别在于 Err 分支会给你一个真正的错误值,你可以检查它、报告它,而不只是“什么都没有”。

fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("不能除以零"))
} else {
Ok(a / b)
}
}
fn main() {
match divide(10.0, 2.0) {
Ok(result) => println!("结果:{result}"),
Err(e) => println!("错误:{e}"),
}
match divide(10.0, 0.0) {
Ok(result) => println!("结果:{result}"),
Err(e) => println!("错误:{e}"),
}
}

输出:

结果:5
错误:不能除以零
▶ 动手试一试

返回 Result<f64, String> 会强制每个调用者面对“可能失败”这件事:你不可能一不小心把除以零的结果当成普通数字来用。

把好几个可能失败的操作串起来,很容易写出层层嵌套的 match。? 运算符能把它拍平。它的意思是:“如果这是 Err,就立刻从当前函数返回这个错误;否则,把 Ok 里面的值给我。”

fn parse_and_double(input: &str) -> Result<i32, std::num::ParseIntError> {
let number = input.parse::<i32>()?; // 解析失败就提前返回
Ok(number * 2)
}
fn main() {
println!("{:?}", parse_and_double("21"));
println!("{:?}", parse_and_double("oops"));
}

输出:

Ok(42)
Err(ParseIntError { kind: InvalidDigit })
▶ 动手试一试

如果没有 ?,这一行就得单独写一个 match,要么取出 number,要么带着错误提前返回。? 用一个字符就做完了这件事。它只能用在本身返回 Result(或 Option)的函数里,因为这样错误才有地方可以返回。

Rust 还有 panic!,它会立刻让程序停下来,下一页会详细讲。一个简单的判断标准:对于调用者有理由想要处理的失败(输入有误、文件不存在),用 Result;panic! 留给那些说明代码本身有 bug 的情况,这时候继续运行比停下来更糟。

当你的函数会以好几种不同的方式失败时,一个简单的 String 错误就不够用了。自定义错误类型那一页会教你定义自己的错误类型,anyhow 与 thiserror 那一页则介绍大多数 Rust 项目用来减少样板代码的 crate。

写一个函数 fn parse_age(input: &str) -> Result<u32, String>。如果文本不是数字,或者数字大于 150,就返回带有清楚提示信息的 Err。用 "34"、"abc" 和 "200" 测试它。提示:对 input.parse::<u32>() 做 match,在 Ok 分支里用 if 检查大小。

查看答案
fn parse_age(input: &str) -> Result<u32, String> {
match input.parse::<u32>() {
Ok(age) => {
if age > 150 {
Err(format!("{age} 岁太大了,不太可能是真的"))
} else {
Ok(age)
}
}
Err(_) => Err(format!("“{input}”不是数字")),
}
}
fn main() {
for input in ["34", "abc", "200"] {
match parse_age(input) {
Ok(age) => println!("年龄:{age}"),
Err(e) => println!("错误:{e}"),
}
}
}

输出:

年龄:34
错误:“abc”不是数字
错误:200 岁太大了,不太可能是真的
▶ 动手试一试

写一个函数 fn add_strings(a: &str, b: &str) -> Result<i32, std::num::ParseIntError>,解析两个字符串并返回它们的和。用 ?,不要用 match。分别打印 ("10", "20") 和 ("10", "twenty") 的结果。提示:每个 parse 后面跟一个 ?,最后一行用 Ok(...) 把和包起来。

查看答案
fn add_strings(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> {
let x = a.parse::<i32>()?;
let y = b.parse::<i32>()?;
Ok(x + y)
}
fn main() {
println!("{:?}", add_strings("10", "20"));
println!("{:?}", add_strings("10", "twenty"));
}

输出:

Ok(30)
Err(ParseIntError { kind: InvalidDigit })
▶ 动手试一试
  • Result<T, E> 表示一个可能成功(Ok)、也可能失败(Err,带着错误值)的操作。
  • match 显式地处理两种情况,和 Option 的写法一样。
  • ? 运算符会自动提前返回 Err,所以不需要在每个可能失败的调用后面都写一个 match。
  • 对于调用者有理由恢复的错误,优先用 Result,而不是 panic!。

小测验

1. Result<T, E> 的两个变体是什么?

2. 在返回 Result 的函数里,? 运算符做了什么?

3. 按照这一页的说法,什么时候应该优先用 Result 而不是 panic!?

4. 在本页的 divide 例子中,divide(10.0, 0.0) 返回什么?

5. 为什么 ? 运算符只能用在本身返回 Result(或 Option)的函数里?

得分: 0 / 5