最近在学习总结Rust的各种场景的语法模式,也就是Rust写代码的模式。
今天分享关于Rust的错误处理的三大类语法模式。
先列出一个大纲
第一类:有意不处理错误,忽略错误
- unwrap()
- .fn( )? 符号,代替rust早期版本中的try!宏
第二类:对错误做自定义信息提示
- 使用expect()。
第三类:推荐!根据正确和错误情况分开处理,错误还可以进一步分流处理
- match(包括 match处理 Result<T,E>或 match处理Option<>, 或 使用map_err())
- 使用if let Some(value)= fn() {} else {}
- 使用特定的函数:and_then() 和 or_else()
我对Rust的错误处理的印象
Rust的错误处理方式比起Golang更灵活,可以针对错误和当下代码需要赋值前做错误判断、故意忽略异常、异常时打印错误并终止等的不同编码场景,选用不同的语法模式。
错误处理涉及到数据类型、错误处理的控制语法、相关的crate模块。
Rust错误处理涉及到的数据类型
错误处理的类型1:Result
Result属于Rust的核心crate提供的功能(core::result::Result)。Result 有2个特点:
首先,Result<T,E> 属于泛型,所以T和E可以是任意类型。但常见的使用模式是以T存储正常情况的信息,E存储错误和异常情况的信息。
其次,Result是枚举类型,其内部实际只包含:
Ok()
Err()
这两个枚举,所以它的实例只会是Ok()或Err()之一。
Result的代码实现中就用的枚举类型的,代码如下:
/// Result的定义在 rust核心代码 src/rust/library/core/src/result.rs 代码文件中:
/// `Result` is a type that represents either success ([`Ok`]) or failure ([`Err`]).
///
/// See the [module documentation](self "module documentation") for details.
#[derive(Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)]
#[must_use = "this `Result` may be an `Err` variant, which should be handled"]
#[rustc_diagnostic_item = "Result"]
#[stable(feature = "rust1", since = "1.0.0")]
pub enum Result<T, E> {
/// Contains the success value
#[lang = "Ok"]
#[stable(feature = "rust1", since = "1.0.0")]
Ok(#[stable(feature = "rust1", since = "1.0.0")] T),
/// Contains the error value
#[lang = "Err"]
#[stable(feature = "rust1", since = "1.0.0")]
Err(#[stable(feature = "rust1", since = "1.0.0")] E),
}
实际使用的一个例子:例如Result<String, &str> 是一个泛型类型,则此时的T类型为String,E类型为&str。它表示一个可能成功或失败的操作的结果,其中成功时的返回类型为 String,失败时的错误类型为 &str。
总结一下Result<Ok(),Err()>: result 类型通常用于表示可能成功或失败的操作的结果。它使用 Result<T, E> 类型来表示,其中 T 是成功时的返回类型,E 是失败时的错误类型。
错误处理的类型2:Option
Option也是枚举类型,其内部实际只包含两种值:Some()和None,这个可以从其代码中得到印证。Option的实现代码为:
/// 代码文件 rustlib/src/rust/library/core/src/option.rs 定义了Option
/// The `Option` type. See [the module level documentation](self "the module level documentation") for more.
#[derive(Copy, PartialOrd, Eq, Ord, Debug, Hash)]
#[rustc_diagnostic_item = "Option"]
#[lang = "Option"]
#[stable(feature = "rust1", since = "1.0.0")]
pub enum Option<T> {
/// No value.
#[lang = "None"]
#[stable(feature = "rust1", since = "1.0.0")]
None,
/// Some value of type `T`.
#[lang = "Some"]
#[stable(feature = "rust1", since = "1.0.0")]
Some(#[stable(feature = "rust1", since = "1.0.0")] T),
}
Result 和 Option 有什么区别?
首先,从名称上理解一下:
- Result 中文意思是结果。一般表示成功或失败,所以它的枚举值会是Ok()和Err();
- Option,中文意思是选项。那么它的侧重点不是成功失败,而是不同的选项(有选项则为Some()、无选项则为None),所以它的枚举值也很清楚,只有Some()和None两个值。
其次,它们之间可以互相转化。对于错误处理来说,match处理Result或Option时,都要处理错误或None等异常值,就实现了Rust的最重要的错误处理逻辑。
错误处理的控制语法的具体分析和举例
1.Rust的第一类错误处理模式:忽略错误,不处理错误:
(1) unwrap()
在 Rust 中,unwrap() 方法用于从 Result 类型中提取成功时的返回值。如果 Result 类型的值是 Ok(表示成功),则 unwrap() 方法将返回 T;如果 Result 类型的值是 Err(表示失败),则 unwrap() 方法将触发一个 panic,抛出一个 E 类型的错误。如果您在调用 unwrap() 方法时遇到错误,说明您正在处理一个 Err 类型的值,即失败的情况。在这种情况下,您应该使用其他方法来处理错误,而不是直接使用 unwrap() 方法。总之,在处理 Result 类型时,应该始终考虑可能的失败情况,并使用适当的方法来处理错误。直接使用 unwrap() 方法可能会导致程序崩溃(panic),因此应该谨慎使用。只应该在非生产的代码中使用。
(2) .fn()?符号
这个符号在Rust中的术语是“提前返回选项”(early return option),作用等同于unwrap()。只允许用于返回Result<>或Option<>类型的函数之后。在rust的早期版本中,有个try!宏具有等效的功能。
代码演示:(演示打开文件,返回Result<File, Error> 类型的值,然后可以被main()中的match方式处理):
use std::fs::File;
use std::io::Error;
fn open_file(file_path: &str) -> Result<File, Error> {
let mut file = File::open(file_path)?;
Ok(file)
}
这段函数内部使用File::open(file_path)?; 打开指定路径的文件,open()是rust的内部函数,原始定义为:
pub fn open<P>(path: P) -> io::Result<File>
而演示代码中有意忽略了错误的情况,以?结尾,来调用open()函数:
let mut file = File::open(file_path)?;
2.Rust的第二类错误处理模式:对错误做自定义信息提示:
expect() 可作为代替unwrap()或 ? 相比unwrap(), expect()是一个更好的选择,因为它允许发生错误时打印一个简单的消息并终止运行。
3.Rust的第三类错误处理模式(推荐!):
根据正确和错误情况分开处理,错误还可以进一步分流处理。
(1) match,根据正确和错误情况分开处理。
- 使用match 分流处理 Result<T,E>中包含错误的情况处理:
use std::fs::File;
use std::io::Error;
/// 演示打开文件,返回Result<File, Error> 类型的值,然后被main()中的match方式处理
fn open_file(file_path: &str) -> Result<File, Error> {
let mut file = File::open(file_path)?;
Ok(file)
}
fn main() {
let file_path = "file.txt";
let file = open_file(file_path);
match file {
Ok(file) => println!("文件打开成功 {:?}", file),
Err(error) => println!("文件打开失败 {}", error),
}
}
- 使用 match 分流处理 Option<>包含错误的情况处理:
use std::fs::File;
use std::io::ErrorKind;
use std::io::{Error, Read};
/// 演示文件打开时 如何返回Option<T> 类型值
fn open_file(file_path: &str) -> Option<File> {
let mut file = File::open(file_path).unwrap();
Some(file)
}
/// 演示 match 如何处理 Option<T> 类型值,其中有None类型的情况
fn read_file(file: Option<File>) -> Result<String, Error> {
match file {
//处理文件的正常情况
Some(mut file) => {
let mut buffer = String::new();
let file_content = file.read_to_string(&mut buffer);
Ok(buffer)
}
//处理文件的异常情况
None => Err(Error::new(ErrorKind::NotFound, "File not found")),
}
}
fn main() {
let file_path = "file.txt";
let file = open_file(&file_path);
let strings_in_file = read_file(file);
/// 在文件 file.txt 不存在的情况下,以下代码会导致软件崩溃。
println!("{}", strings_in_file.unwrap());
///echo "1111">> file.txt
/// 创建 file.txt文件
/// 然后重复上面代码
}
- 使用 map_err() 链式处理
map_err() 将在以后发布的文章中再讲解。本文不做详细介绍。
(2) if let,适合直接在赋值前做错误处理。代码模式为:
let final_value = if let Some(T) = Rust语句 {
//语句正确和成功的情况,如获取有效数据,将作为作用域的返回值赋值给final_value
} else {
//错误或异常的情况的处理,如赋值为"",同样会作为作用域的返回值赋值给 final_value
}
以下是一些常见的处理错误的方法:使用 match 表达式:通过使用 match 表达式,您可以根据 Result 类型的值来执行不同的操作。如果是 Ok 类型,可以提取成功的值;如果是 Err 类型,可以处理错误。使用 map_err() 方法:map_err() 方法可以将 Err 类型的值转换为另一种错误类型,并返回一个新的 Result 类型。使用 and_then() 或 or_else() 方法:这些方法可以在成功或失败的情况下执行不同的操作,并返回一个新的 Result 类型。
(3) 使用特定的函数:and_then() 和 or_else()和 ok_or()
这3个函数在Rust中的术语为组合算子,如果你已理解C/C++中的 && 和 ||或 Python中的and以及or语法的意义,那么你大概已经理解了 and_then() 这3个函数的意思。比如 and_then()是当调用者为true或调用者为正常的时候,才会调用and_then(...)函数。那么对于错误处理就非常有用。
下面的代码例子演示了烹饪的逻辑:当有食材的时候,才能按照食谱制作好菜品。隐含的意思就是(错误的情况下),没有食材的情况下,就不用照着食谱做菜了。
#![allow(dead_code)]
#[derive(Debug)] enum Food { CordonBleu, Steak, Sushi }
#[derive(Debug)] enum Day { Monday, Tuesday, Wednesday }
// 我们没有原材料(ingredient)来制作寿司。
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),
}
}
// 做一份好菜,我们需要原材料和食谱这两者。
// 我们可以借助一系列 `match` 来表达相应的逻辑:
// (原文:We can represent the logic with a chain of `match`es:)
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);
}