Rust可靠性也延申到错误处理,错误处理是预测并处理故障可能性的过程。
-
大部分情况下:在编译时提示错误并处理 错误的分类: -
可恢复的 -
例如:文件未找到,可再次尝试 -
不可恢复的 -
Bug,例如访问的索引超出范围 Rust中没有类似异常的机制 -
针对可恢复的错误:使用Result<T,E> -
不可恢复的错误:使用Panic!宏
例如,如果程序无法读取某个文件,然后继续使用该错误输入,则明显会产生错误。 如果你能注意到并显式管理这些错误,可以使程序避免进一步的陷阱。
-
使用 panic!
处理无法恢复的错误。 -
如果值是可选的或缺少值不是一种错误情况,请使用 Option
枚举。 -
当可能出现问题并且调用方必须处理问题时,请使用 Result
枚举。
不可恢复的错误与Panic
panic 是 Rust 中最简单的错误处理机制。 当Panic!宏执行时:
-
你的程序会打印一个错误信息 -
展开(unwind)、清理调用栈(stack) -
不进行清理,直接停止程序 -
但是需要操作系统os稍后进行清理 -
Rust沿着调用栈往回走 -
清理每个遇到的函数中的数据 -
程序展开调用栈(工作量大) -
或立即终止调用栈 -
退出程序
手动调用panic!
你可以使用 panic!
宏来使当前线程 panic。 宏将输出一条错误消息、清理资源,然后退出程序。
fn main(){
panic!("Farewell!")
}
程序将以状态代码101退出,并输出以下的消息:
上述 panic 消息的最后一部分显示了发生 panic 的位置。 它发生在 src/main.rs 文件中第二行的第五个字符处。
一般来说,当程序进入不可恢复状态时,应该使用 panic!
。 一个完全无法从错误中恢复的状态。
程序出错自动panic!
Rust 在执行某些操作(例如被零除或试图访问数组、矢量或哈希映射中不存在的索引)时崩溃,如以下代码所示:
let v = vec![0, 1, 2, 3];
v[6]; // this will cause a panic!
上面的报错信息提示,panic出现在第6行5个字符的位置,即v[6],出错文件为main.rs,但是main.rs中并未出现panic!的使用,说明真实的报错位置不是这里 报错信息中提示,可以将RUST_BACKTRACE设置为1,就可以显示回溯信息
// 在命令行终端下输入如下命令设置
set RUST_BACKTRACE=1 && cargo run
为了获取带有调试信息的回溯,必须启用调试符号(不带 --release)
设置panic的终止情况
如果想让二进制文件更小,可以设置从"展开"改为"中止":
-
在Cargo.toml中适当的profile部分设置 -
panic = 'abort' -
可恢复的错误与Result枚举
Rust 提供了用于返回和传播错误的 Result<T, E>
枚举。 按照惯例,Ok(T)
变量表示成功并包含一个值,而变量 Err(E)
表示错误并包含一个错误值。
Result<T, E>
枚举定义为:
enum Result<T, E> {
Ok(T): // A value T was obtained.
Err(E): // An error of type E was encountered instead.
}
-
T:操作成功情况下,Ok变体里返回的数据的类型 -
E:操作失败情况下,Err变体里返回的数据的类型
不同于描述缺少某个值的可能性的 Option
类型,Result
类型最适合在可能会失败时使用。
Result
类型还具有 unwrap
和 expect
方法,这些方法执行以下操作之一:
-
返回 Ok
变量中的值。 -
如果变体是 Err
,则导致程序 panic。
让我们看一下实际操作中的 Result
。 下面的示例代码中实现了 safe_division
函数,该函数返回以下任一内容:
-
变体为 Ok
的Result
值,该变体携带了成功的除法运算的结果。 -
一个 Err
变体,它携带了一个结构DivisionByZeroError
,该结构指示除法运算不成功。
#[derive(Debug)]
struct DivisionByZeroError;
fn safe_division(dividend: f64, divisor: f64) -> Result<f64, DivisionByZeroError> {
if divisor == 0.0 {
Err(DivisionByZeroError)
} else {
Ok(dividend / divisor)
}
}
fn main() {
println!("{:?}", safe_division(9.0, 3.0));
println!("{:?}", safe_division(4.0, 0.0));
println!("{:?}", safe_division(0.0, 2.0));
}
DivisionByZeroError
结构之前的 #[derive(Debug)]
部分是一个宏,指示 Rust 编译器将类型设置为可输出,以便进行调试。 我们稍后会在“特征”模块中深入讨论此概念。
使用Result的一种方式:match表达式
和Option枚举一样,Result及其变体也是有prelude带入作用域的
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
match f {
Ok(file) => println!("{:?}", file),
Err(err) => panic!("{}", err),
}
}
当文件存在时返回文件,当文件不存在时输出报错信息
匹配不同的错误
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(err) => match err.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Error creating file: {:?}", e),
},
other_error => panic!("Error opening the file: {:?}", other_error)
},
};
}
上面的例子中使用了很多match,match很有用,但是很原始 闭包(closure)。Result<T,E>有很多方法 :
-
它们接收闭包作为参数 -
使用match实现 简化后的代码如下
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f =File::open("hello.txt").unwrap_or_else(|error|{
if error.kind() == ErrorKind::NotFound{
File::create("hello.txt").unwrap_or_else(|error|{
panic!("error creating file: {:?}", error);
})
} else {
panic!("error opening file: {:?}", error)
}
});
}
unwrap
unwrap:match表达式的一个快捷方法
-
如果Result结果是Ok,返回就是ok里的值 -
如果Result结果是Err,就会调用panic!宏 -
缺点是panic的报错信息无法自定义
let f = File::open("hello.txt");
let f = match f{
Ok(file) => file,
Err(err) => panic!("error opening file: {:?}", err),
};
// 使用unwrap简化上面的代码
let f = File::open("hello.txt").unwrap();
expect
expect:和unwrap类似,但是可以指定错误信息
let f = File::open("hello.txt").expect("无法打开文件");
传播错误 与? 表达式
错误的处理方式:
-
在函数中处理错误 -
将错误返回给调用者 以下的例子展示如何将错误返回给调用者
use std::fs::File;
use std::io;
use std::io::Read;
fn main() {
let result= read_username_from_file();
println!("{:?}", result);
}
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),
}
}
由于在rust中传播错误的情况很多,有?运算符可用于简化代码 ?运算符可以用在result枚举后,含义是当result的结果是ok时,ok中的值就是表达式的结果,代码正常运行,如果result的结果是err,则将err作为整个函数的结果返回,相当于使用return
fn main() {
let result= read_username_from_file();
println!("{:?}", result);
}
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)
}
?运算符与from函数
Trait std::convert::From 上的from函数,用于错误之间的转换 被?所应用的错误,会隐式的被from函数处理 当?调用from函数时:
-
它所接收的错误类型会被转化为当前函数返回类型所定义的错误类型 用于:针对不同错误原因,返回同一种错误类型 -
只要每个错误类型实现了转换为所返回的错误类型的from函数
继续优化上面的代码,?运算符可以用链式表达
use std::fs::File;
use std::io;
use std::io::Read;
fn main() {
let result= read_username_from_file();
println!("{:?}", result);
}
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都返回ok的时候,才会向下运行,如果出现err,程序就会返回? 运算符只能用于返回类型为result的函数
?运算符与from函数
mian函数默认的返回类型是(),也可以将main函数的返回类型修改为result
fn main() -> Result<(),Box<dyn Error>>{
let f = File::open("hello.txt")?;
Ok(())
}
Box<dyn Error>>
是trail对象,可以简单的理解为:任何可能的错误类型
使用Option类型来处理Panic
Rust 标准库提供了在可能缺少值的情况下可以使用的 Option<T>
枚举。 Option<T>
在 Rust 代码中的使用非常广泛。 它可用于处理可能存在或可能为空的值。
在许多其他语言中,缺少值的情况将使用 null
或 nil
进行建模,但 Rust 不会在使用其他语言互操作的代码之外使用 null
。 Rust 明确何时值是可选的。 尽管在很多语言中,采用 String
的函数实际上可能采用 String
或 null
,而在 Rust 中,同一函数只能采用实际的 String
。 如果要在 Rust 中对可选字符串建模,则需要将其显式包装在 Option
类型中:Option<String>
。
Option<T>
将自身列为两个变体之一:
eunm Option<T>{
None, //这个值不存在
Some(T), //这个值存在
}
Option<T>
枚举声明的 <T>
部分声明类型 T
是通用的,将与 Option
枚举的 Some
变体相关联。
如前面几节中所述,None
和 Some
不是类型,而是 Option<T>
类型的变体。这表示在其他功能中,函数不能使用 Some
或 None
作为参数,而只能使用 Option<T>
作为参数。
在前面的例子中,我们提到尝试访问矢量的不存在的索引会导致程序 panic
,但你可以通过使用 Vec::get
方法(该方法返回 Option
类型,而不是 panic)来避免这种情况。 如果该值存在于指定的索引处,系统会将其包装在 Option::Some(value)
变体中。 如果索引超出界限,它会改为返回 Option::None
值。
let fruits = vec!["banana", "apple", "coconut", "orange", "strawberry"];
// pick the first item:
let first = fruits.get(0);
println!("{:?}", first);
// pick the third item:
let third = fruits.get(2);
println!("{:?}", third);
// pick the 99th item, which is non-existent:
let non_existent = fruits.get(99);
println!("{:?}", non_existent);
输出的消息指出,访问 fruits
数组中现有索引的前二次尝试导致了 Some("banana")
和 Some("coconut")
,但尝试提取第 99 个元素返回了 None
值(它与任何数据都不关联)而不是 panic。
实际上,你必须根据程序获得的枚举变体来决定程序的行为方式。 但如何才能访问 Some(data)
变体中的数据呢?
match模式匹配
Rust 中提供了一个功能强大的运算符,称为 match
。 可利用该运算符,通过提供模式来控制程序流。 当 match
找到匹配的模式时,它会运行随该模式一起提供的代码。
let fruits = vec!["banana", "apple", "coconut", "orange", "strawberry"];
for &index in [0, 2, 99].iter() {
match fruits.get(index) {
Some(fruit_name) => println!("It's a delicious {}!", fruit_name),
None => println!("There is no fruit! :("),
}
}
在前面的代码中,我们循环访问上一个示例中的相同索引(0、2 和 99),然后通过 fruits.get(index) 表达式使用每个索引检索 fruits
矢量中的值。
由于 fruits
矢量包含 &str
元素,因此,我们知道此表达式的结果是 Option<&str>
类型的。 然后,你对 Option
值使用 match 表达式,并为它的每个变体定义一个操作过程。 Rust 将这些分支称为“match arm”,每个 arm 可以处理匹配值的一个可能结果。
第一个分支引入了一个新变量 fruit_name
。 此变量与 Some
值中的任何值匹配。 fruit_name
的范围仅限于 match 表达式,因此在将 fruit_name
引入到 match
之前声明它并没有意义。
你可以进一步细化 match 表达式,以根据 Some
变体中的值执行不同的操作。 例如,你可以通过运行以下代码来强调椰子很棒这个事实:
let fruits = vec!["banana", "apple", "coconut", "orange", "strawberry"];
for &index in [0, 2, 99].iter() {
match fruits.get(index) {
Some(&"coconut") => println!("Coconuts are awesome!!!"),
Some(fruit_name) => println!("It's a delicious {}!", fruit_name),
None => println!("There is no fruit! :("),
}
}
当字符串值为 "coconut"
时,将匹配第一个 arm,然后使用它来确定执行流。
当你使用 match 表达式时,请记住以下规则:
-
按照从上到下的顺序对 match
arm 进行评估。 必须在一般事例之前定义具体事例,否则它们将无法进行匹配和评估。 -
match
arm 必须涵盖输入类型可能具有的每个可能值。 如果你尝试根据非详尽模式列表进行匹配,则会出现编译器错误。
❝
匹配中的第一个模式是
Some(&"coconut")
(请注意字符串文本前的&
)。 这是因为fruits.get(index)
会返回一个Option<&&str>
或一个对字符串切片的引用的选项。 删除模式中的&
意味着我们将尝试依据Option<&str>
(一个可选字符串切片而不是一个对字符串切片的可选引用)进行匹配。 我们尚未介绍引用,因此现在这还没有太大意义。 现在,只需记住&
可确保类型正确对齐。❞
if let 表达式
Rust 提供了一种方便的方法来测试某个值是否符合单个模式。5
在下面的示例中,match
的输入是一个 Option<u8>
值。 match
表达式应仅在该输入值为 7 时运行代码。
let a_number: Option<u8> = Some(7);
match a_number {
Some(7) => println!("That's my lucky number!"),
_ => {},
}
在这种情况下,我们想忽略 None
变体以及与 Some(7)
不匹配的所有 Some<u8>
值。 通配符模式适用于此类情况。 你可以在所有其他模式之后添加 _
(下划线)通配符模式,以匹配任何其他项,并使用它来满足编译器耗尽 match arm 的需求。
若要压缩此代码,可使用 if let 表达式:
let a_number: Option<u8> = Some(7);
if let Some(7) = a_number {
println!("That's my lucky number!");
}
if let 运算符可将模式与表达式进行比较。 如果表达式与模式匹配,则会执行 if 块。 if let 表达式的好处是,当你关注的是要匹配的单个模式时,你不需要 match 表达式的所有样板代码。
使用unwrap
和expect
你可以尝试使用 unwrap
方法直接访问 Option
类型的内部值。 但是要小心,因为如果变体是 None
,则此方法将会 panic。
例如:
let gift = Some("candy");
assert_eq!(gift.unwrap(), "candy");
let empty_gift: Option<&str> = None;
assert_eq!(empty_gift.unwrap(), "candy"); // This will panic!
在这个例子中,代码会Panic,输出如下:
thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', src/main.rs:6:27
expect
方法的作用与 unwrap
相同,但它提供由第二个参数提供的自定义 panic 消息:
let a = Some("value");
assert_eq!(a.expect("fruits are healthy"), "value");
let b: Option<&str> = None;
b.expect("fruits are healthy"); // panics with `fruits are healthy`
输出显示:
thread 'main' panicked at 'fruits are healthy', src/main.rs:6:7
因为这些函数可能会崩溃,所以不建议使用它。 请改为考虑使用下列方法之一:
-
使用模式匹配并显式处理 None
案例。 -
调用类似的非 panic 方法,例如 unwrap_or
。如果变体为None
,则该方法会返回默认值;如果变体为Some(value)
,则会返回内部值。
assert_eq!(Some("dog").unwrap_or("cat"), "dog");
assert_eq!(None.unwrap_or("cat"), "cat");
什么时候应该用panic!
总体原则:
-
在定义一个可能失败的函数时,优先考虑使用返回result -
否则就Panic 建议使用panic的场景: -
编写示例:unwrap -
原型代码:unwrap、expect -
测试:unwrap、expect 如果你可以确定Result的结果一定就是OK,那么就可以使用unwrap,这样肯定不会出现panic
错误处理的指导性建议
-
当代码最终可能处于损坏状态时,最好使用panic -
损坏状态(Bad state):某些假设、保证、约定或不可变性被打破 -
这种损坏状态并不是预期能够偶尔发生的事情 -
在此之后,你的代码如果处于损坏状态就无法运行 -
在您使用的类型中没有一个好的方法来将这些信息(处于损坏状态)进行编码 场景建议: -
例如非法的值、矛盾的值或空缺的值被传入代码 -
以及下列的一条: -
调用代码:传入无意义的参数值,Panic -
调用外部不可控代码,返回非法状态,您无法修复:panic -
如果失败是可预期的:result -
当你的代码对值进行操作,首先应该验证这些值:panic
原文始发于微信公众号(宁雪):Rust的错误处理机制
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论