rust学习笔记之基础:错误处理
错误处理
Rust 将错误组合成两个主要类别:可恢复错误(recoverable)和 不可恢复错误(unrecoverable)。可恢复错误通常代表向用户报告错误和重试操作是合理的情况,比如未找到文件。不可恢复错误通常是 bug 的同义词,比如尝试访问超过数组结尾的位置。
大部分语言并不区分这两类错误,并采用类似异常这样方式统一处理他们。Rust 并没有异常,但是有可恢复错误 Result<T, E> 和不可恢复(遇到错误时停止程序执行)错误 panic!。
panic! 与不可恢复的错误
当出现 panic 时,程序默认会开始展开,这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。另一种选择是直接终止,这会不清理数据就退出程序,那么程序所使用的内存需要由操作系统来清理。
如果你需要项目的最终二进制文件越小越好,panic 时通过在 Cargo.toml 的 [profile] 部分增加 panic = 'abort',可以由展开切换为终止。例如,如果你想要在 release 模式中 panic 时直接终止:
[profile.release]
panic = 'abort'
Rust 有 panic!宏。当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,然后接着退出。出现这种情况的场景通常是检测到一些类型的 bug,而且开发者并不清楚该如何处理它。
fn main() {
panic!("crash and burn");
}
Option 和 unwrap
在标准库中有个叫做 Option<T> 的枚举类型,用于有 “不存在” 的可能性的情况。它表现为以下两个 “option”中 的一个:
- Some(T):找到一个属于 T 类型的元素
- None:找不到相应元素
这些选项可以通过 match 显式地处理,或使用 unwrap 隐式地处理。隐式处理要么返回 Some 内部的元素,要么就 panic。
fn give_commoner(gift: Option<&str>) {
// 指出每种情况下的做法。
match gift {
Some("snake") => println!("Yuck! I'm throwing that snake in a fire."),
Some(inner) => println!("{}? How nice.", inner),
None => println!("No gift? Oh well."),
}
}
fn give_princess(gift: Option<&str>) {
let inside = gift.unwrap(); // `unwrap` 在接收到 `None` 时将返回 `panic`。
if inside == "snake" { panic!("AAAaaaaa!!!!"); }
println!("I love {}s!!!!!", inside);
}
fn main() {
let food = Some("chicken");
let snake = Some("snake");
let void = None;
give_commoner(food);
give_commoner(snake);
give_commoner(void);
let bird = Some("robin");
let nothing = None;
give_princess(bird);
give_princess(nothing);
}
使用 ? 解开 Option
你可以使用 match 语句来解开 Option,但使用 ? 运算符通常会更容易。如果 x 是 Option,那么若 x 是 Some ,对 x?表达式求值将返回底层值,否则无论函数是否正在执行都将终止且返回 None。
fn next_birthday(current_age: Option<u8>) -> Option<String> {
// 如果 `current_age` 是 `None`,这将返回 `None`。
// 如果 `current_age` 是 `Some`,内部的 `u8` 将赋值给 `next_age`。
let next_age: u8 = current_age?;
Some(format!("Next year I will be {}", next_age))
}
可以将多个 ? 链接在一起,以使代码更具可读性。
struct Person {
job: Option<Job>,
}
#[derive(Clone, Copy)]
struct Job {
phone_number: Option<PhoneNumber>,
}
#[derive(Clone, Copy)]
struct PhoneNumber {
area_code: Option<u8>,
number: u32,
}
impl Person {
// 获取此人的工作电话号码的区号(如果存在的话)。
fn work_phone_area_code(&self) -> Option<u8> {
// 没有`?`运算符的话,这将需要很多的嵌套的 `match` 语句。
self.job?.phone_number?.area_code
}
}
fn main() {
let p = Person {
job: Some(Job {
phone_number: Some(PhoneNumber {
area_code: Some(61),
number: 439222222,
}),
}),
};
assert_eq!(p.work_phone_area_code(), Some(61));
}
组合算子
match 是处理 Option 的一个可用的方法,但你会发现大量使用它会很繁琐,特别是当操作只对一种输入是有效的时。这时可以使用组合算子,以模块化的风格来管理控制流。Option 有一个内置方法 map(),这个组合算子可用于 Some -> Some 和 None -> None 这样的简单映射。多个不同的 map() 调用可以串起来,这样更加灵活。
Some(3).map(|x| x * 2); // 结果为 Some(6)
例子
#![allow(dead_code)]
#[derive(Debug)] enum Food { Apple, Carrot, Potato }
#[derive(Debug)] struct Peeled(Food);
#[derive(Debug)] struct Chopped(Food);
#[derive(Debug)] struct Cooked(Food);
// 这个函数会完成削皮切块烹饪
fn process(food: Option<Food>) -> Option<Cooked> {
food.map(|f| Peeled(f))
.map(|Peeled(f)| Chopped(f))
.map(|Chopped(f)| Cooked(f))
}
// 在尝试吃食物之前确认食物是否存在是非常重要的!
fn eat(food: Option<Cooked>) {
match food {
Some(food) => println!("Mmm. I love {:?}", food),
None => println!("Oh no! It wasn't edible."),
}
}
fn main() {
let apple = Some(Food::Apple);
let potato = None;
let cooked_apple = process(apple);
let cooked_potato = process(potato);
eat(cooked_apple);
eat(cooked_potato);
}
map() 以链式调用的方式来简化 match 语句。然而如果以返回类型是 Option<T> 的函数作为 map() 的参数,会导致出现嵌套形式 Option<Option<T>>。这样多层串联调用就会变得混乱。所以引入 and_then(),and_then() 使用被 Option 包裹的值来调用其输入函数并返回结果。 如果 Option 是 None,那么它返回 None。
Some(5).and_then(|x| Some(x + 1)); // 结果为 Some(6)
例子
#![allow(dead_code)]
#[derive(Debug)] enum Food { CordonBleu, Steak, Sushi }
#[derive(Debug)] enum Day { Monday, Tuesday, Wednesday }
fn have_ingredients(food: Food) -> Option<Food> {
match food {
Food::Sushi => None,
_ => Some(food),
}
}
fn have_recipe(food: Food) -> Option<Food> {
match food {
Food::CordonBleu => None,
_ => Some(food),
}
}
fn cookable_v1(food: Food) -> Option<Food> {
match have_ingredients(food) {
None => None,
Some(food) => match have_recipe(food) {
None => None,
Some(food) => Some(food),
},
}
}
// 也可以使用 `and_then()` 把上面的逻辑改写得更紧凑:
fn cookable_v2(food: Food) -> Option<Food> {
have_ingredients(food).and_then(have_recipe)
}
fn eat(food: Food, day: Day) {
match cookable_v2(food) {
Some(food) => println!("Yay! On {:?} we get to eat {:?}.", day, food),
None => println!("Oh no. We don't get to eat on {:?}?", day),
}
}
fn main() {
let (cordon_bleu, steak, sushi) = (Food::CordonBleu, Food::Steak, Food::Sushi);
eat(cordon_bleu, Day::Monday);
eat(steak, Day::Tuesday);
eat(sushi, Day::Wednesday);
}
Result 与可恢复的错误
Result 是 Option 类型的更丰富的版本,描述的是可能的错误而不是可能的不存在。也就是说,Result<T,E> 可以有两个结果的其中一个:
Ok<T>:找到 T 元素Err<E>:找到 E 元素,E 即表示错误的类型。
按照约定,预期结果是 “Ok”,而意外结果是 “Err”。
大部分错误并没有严重到需要程序完全停止执行。一般地,我们希望把错误返回给调用者,这样它可以决定回应错误的正确方式。
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => {
panic!("Problem opening the file: {:?}", error)
},
};
}
注意与 Option 枚举一样,Result 枚举和其成员也被导入到了 prelude 中,所以就不需要在 match 分支中的 Ok 和 Err 之前指定 Result::。
这里我们告诉 Rust 当结果是 Ok 时,返回 Ok 成员中的 file 值,然后将这个文件句柄赋值给变量 f。match 之后,我们可以利用这个文件句柄来进行读写。match 的另一个分支处理从 File::open 得到 Err 值的情况。在这种情况下,我们选择调用 panic! 宏。
失败时 panic 的简写:unwrap 和 expect
使用 unwrap 隐式地处理:要么返回 Ok 内部的元素,要么就 panic。
use std::fs::File;
fn main() {
let f = File::open("hello.txt").unwrap();
}
使用 expect 而不是 unwrap 并提供一个好的错误信息可以表明你的意图并更易于追踪 panic 的根源。
use std::fs::File;
fn main() {
let f = File::open("hello.txt").expect("Failed to open hello.txt");
}
匹配不同的错误
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 操作可能导致的不同错误类型。
Result 的组合算子
use std::num::ParseIntError;
// 如果值是合法的,计算其乘积,否则返回错误。
fn multiply(first_number_str: &str, second_number_str: &str) -> Result<i32, ParseIntError> {
first_number_str.parse::<i32>().and_then(|first_number| {
second_number_str.parse::<i32>().map(|second_number| first_number * second_number)
})
}
fn print(result: Result<i32, ParseIntError>) {
match result {
Ok(n) => println!("n is {}", n),
Err(e) => println!("Error: {}", e),
}
}
fn main() {
// 这种情况下仍然会给出正确的答案。
let twenty = multiply("10", "2");
print(twenty);
// 这种情况下就会提供一条更有用的错误信息。
let tt = multiply("t", "2");
print(tt);
}
传播错误
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),
}
}
调用这个函数的代码最终会得到一个包含用户名的 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 关键字一样,这样错误值就被传播给了调用者。
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)
}
给 Result 取别名
use std::num::ParseIntError;
// 为带有错误类型 `ParseIntError` 的 `Result` 定义一个泛型别名。
type AliasedResult<T> = Result<T, ParseIntError>;
// 使用上面定义过的别名来表示上一节中的 `Result<i32,ParseIntError>` 类型。
fn multiply(first_number_str: &str, second_number_str: &str) -> AliasedResult<i32> {
first_number_str.parse::<i32>().and_then(|first_number| {
second_number_str.parse::<i32>().map(|second_number| first_number * second_number)
})
}
// 在这里使用别名又让我们节省了一些代码量。
fn print(result: AliasedResult<i32>) {
match result {
Ok(n) => println!("n is {}", n),
Err(e) => println!("Error: {}", e),
}
}
fn main() {
print(multiply("10", "2"));
print(multiply("t", "2"));
}
处理多种错误类型
从 Option 中取出 Result
处理混合错误类型的最基本的手段就是让它们互相包含。
use std::num::ParseIntError;
fn double_first(vec: Vec<&str>) -> Option<Result<i32, ParseIntError>> {
vec.first().map(|first| {
first.parse::<i32>().map(|n| 2 * n)
})
}
fn main() {
let numbers = vec!["42", "93", "18"];
let empty = vec![];
let strings = vec!["tofu", "93", "18"];
println!("The first doubled is {:?}", double_first(numbers));
println!("The first doubled is {:?}", double_first(empty));
println!("The first doubled is {:?}", double_first(strings));
}
输出
The first doubled is Some(Ok(84))
The first doubled is None
The first doubled is Some(Err(ParseIntError { kind: InvalidDigit }))
自定义错误类型
有时候把所有不同的错误都视为一种错误类型会简化代码。Rust 允许我们定义自己的错误类型。一般来说,一个 “好的” 错误类型应当:
- 用同一个类型代表了多种错误
- 向用户提供了清楚的错误信息
- 能够容易地与其他类型比较
- 能够容纳错误的具体信息
- 能够与其他错误很好地整合
use std::error;
use std::fmt;
type Result<T> = std::result::Result<T, DoubleError>;
#[derive(Debug, Clone)]
struct DoubleError;
impl fmt::Display for DoubleError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "invalid first item to double")
}
}
// 为 `DoubleError` 实现 `Error` trait,这样其他错误可以包裹这个错误类型。
impl error::Error for DoubleError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
// 泛型错误,没有记录其内部原因。
None
}
}
fn double_first(vec: Vec<&str>) -> Result<i32> {
vec.first()
.ok_or(DoubleError) // ok_or将Option转换为Result。把错误换成我们的新类型。
.and_then(|s| {
s.parse::<i32>()
.map_err(|_| DoubleError)// map_err转换错误类型。这里也换成新类型。
.map(|i| 2 * i)
})
}
fn print(result: Result<i32>) {
match result {
Ok(n) => println!("The first doubled is {}", n),
Err(e) => println!("Error: {}", e),
}
}
fn main() {
let numbers = vec!["42", "93", "18"];
let empty = vec![];
let strings = vec!["tofu", "93", "18"];
print(double_first(numbers));
print(double_first(empty));
print(double_first(strings));
}
输出
The first doubled is 84
Error: invalid first item to double
Error: invalid first item to double
把错误 “装箱”
如果又想写简单的代码,又想保存原始错误信息,一个方法是把它们装箱(Box)。这样做的坏处就是,被包装的错误类型只能在运行时了解,而不能被静态地判别。对任何实现了 Error trait 的类型,标准库的 Box 通过 From 为它们提供了到 Box<Error> 的转换。
use std::error;
use std::fmt;
// 为 `Box<error::Error>` 取别名。
type Result<T> = std::result::Result<T, Box<dyn error::Error>>;
#[derive(Debug, Clone)]
struct EmptyVec;
impl fmt::Display for EmptyVec {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "invalid first item to double")
}
}
impl error::Error for EmptyVec {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
None
}
}
fn double_first(vec: Vec<&str>) -> Result<i32> {
vec.first()
.ok_or_else(|| EmptyVec.into()) // ok_or_else将Option转换为Result,方法参数为闭包。装箱
.and_then(|s| {
s.parse::<i32>()
.map_err(|e| e.into()) // map_err转换错误类型。装箱
.map(|i| 2 * i)
})
}
fn print(result: Result<i32>) {
match result {
Ok(n) => println!("The first doubled is {}", n),
Err(e) => println!("Error: {}", e),
}
}
fn main() {
let numbers = vec!["42", "93", "18"];
let empty = vec![];
let strings = vec!["tofu", "93", "18"];
print(double_first(numbers));
print(double_first(empty));
print(double_first(strings));
}
输出
The first doubled is 84
Error: invalid first item to double
Error: invalid digit found in string
使用 ? 让代码更清晰
? 实际上是指 unwrap 或 return Err(From::from(err))。由于 From::from 是不同类型之间的转换工具,也就是说,如果在错误可转换成返回类型地方使用 ?,它将自动转换成返回类型。
use std::error;
use std::fmt;
// 为 `Box<error::Error>` 取别名。
type Result<T> = std::result::Result<T, Box<dyn error::Error>>;
#[derive(Debug)]
struct EmptyVec;
impl fmt::Display for EmptyVec {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "invalid first item to double")
}
}
impl error::Error for EmptyVec {}
// 这里的结构和之前一样,但是这次没有把所有的 `Result` 和 `Option` 串起来,而是使用 `?` 立即得到内部值。
fn double_first(vec: Vec<&str>) -> Result<i32> {
let first = vec.first().ok_or(EmptyVec)?;
let parsed = first.parse::<i32>()?;
Ok(2 * parsed)
}
fn print(result: Result<i32>) {
match result {
Ok(n) => println!("The first doubled is {}", n),
Err(e) => println!("Error: {}", e),
}
}
fn main() {
let numbers = vec!["42", "93", "18"];
let empty = vec![];
let strings = vec!["tofu", "93", "18"];
print(double_first(numbers));
print(double_first(empty));
print(double_first(strings));
}
遍历 Result
Iter::map 操作可能失败,比如:
fn main() {
let strings = vec!["tofu", "93", "18"];
let numbers: Vec<_> = strings
.into_iter()
.map(|s| s.parse::<i32>())
.collect();
println!("Results: {:?}", numbers); // Results: [Err(ParseIntError { kind: InvalidDigit }), Ok(93), Ok(18)]
}
使用 filter_map() 忽略失败的项:
fn main() {
let strings = vec!["tofu", "93", "18"];
let numbers: Vec<_> = strings
.into_iter()
.filter_map(|s| s.parse::<i32>().ok())
.collect();
println!("Results: {:?}", numbers); // Results: [93, 18]
}
使用 collect() 使整个操作失败:Result 实现了 FromIter,因此结果的向量(Vec<Result<T, E>>)可以被转换成结果包裹着向量(Result<Vec<T>, E>)。一旦找到一个 Result::Err ,遍历就被终止。同样的技巧可以对 Option 使用。
fn main() {
let strings = vec!["tofu", "93", "18"];
let numbers: Result<Vec<_>, _> = strings
.into_iter()
.map(|s| s.parse::<i32>())
.collect();
println!("Results: {:?}", numbers); // Results: Err(ParseIntError { kind: InvalidDigit })
}
使用 Partition() 收集所有合法的值与错误
fn main() {
let strings = vec!["tofu", "93", "18"];
let (numbers, errors): (Vec<_>, Vec<_>) = strings
.into_iter()
.map(|s| s.parse::<i32>())
.partition(Result::is_ok);
println!("Numbers: {:?}", numbers); // Numbers: [Ok(93), Ok(18)]
println!("Errors: {:?}", errors); // Errors: [Err(ParseIntError { kind: InvalidDigit })]
let numbers: Vec<_> = numbers.into_iter().map(Result::unwrap).collect();
let errors: Vec<_> = errors.into_iter().map(Result::unwrap_err).collect();
println!("Numbers: {:?}", numbers); // Numbers: [93, 18]
println!("Errors: {:?}", errors); // Errors: [ParseIntError { kind: InvalidDigit }]
}
使用 panic! 还是返回 Result
如何决定何时应该 panic! 以及何时应该返回 Result 呢?如果代码 panic,就没有恢复的可能。你可以选择对任何错误场景都调用 panic!,不管是否有可能恢复,不过这样就是你代替调用者决定了这是不可恢复的。选择返回 Result 值的话,就将选择权交给了调用者,而不是代替他们做出决定。调用者可能会选择以符合他们场景的方式尝试恢复,或者也可能干脆就认为 Err 是不可恢复的,所以他们也可能会调用 panic! 并将可恢复的错误变成了不可恢复的错误。因此返回 Result 是定义可能会失败的函数的一个好的默认选择。
浙公网安备 33010602011771号