序
不管是什么语言错误处理都是必须的,JavaScript 提供了try catch
捕获错误,也提供了 Error
通过throw
抛出一个错误。当然rust也提供了类似的处理机制。
错误类型
Rust 将错误分为两大类:可恢复的(recoverable)和 不可恢复的(unrecoverable)错误。
Rust提供了 Result<T, E>
类型,用于处理可恢复的错误,还有 panic!
宏,在程序遇到不可恢复的错误时停止执行。
对于一个可恢复的错误,比如文件未找到的错误,我们很可能只想向用户报告问题并重试操作。不可恢复的错误总是 bug 出现的征兆,比如试图访问一个超过数组末端的位置,因此我们要立即停止程序。
panic! 处理不可恢复的错误
在rust中有两种方法造成 panic:
- 执行会造成代码 panic 的操作(比如访问超过数组结尾的内容)
- 显式调用
panic!
宏。
通常情况下这些 panic 会打印出一个错误信息,展开并清理栈数据,然后退出。
通过一个环境变量,你也可以让 Rust 在 panic 发生时打印调用堆栈(call stack)以便于定位 panic 的原因。
对应 panic 时的栈展开或终止
当panic发生,程序处理有两种选择:
- 程序展开调用栈,rust沿着调用栈,回溯清理每个函数的数据。
- 立即终止调用栈,不进行清理工作,需要后续靠操作系统完成清理。
如果你需要项目的最终二进制文件越小越好,panic 时通过在 Cargo.toml 的 [profile] 部分增加 panic = 'abort',可以由展开切换为终止。例如,如果你想要在 release 模式中 panic 时直接终止:
[profile.release]
panic = 'abort'
我们可以直接使用panic!
宏直接抛出一个错误。和我们JavaScript中 throw new Error()
,作用一样。
fn main() {
panic!("crash and burn");
}
backtrace
backtrace 是一个执行到目前位置所有被调用的函数的列表。RUST_BACKTRACE
环境变量来得到一个 backtrace。
获取backtrace,必须启用 debug 标识。当不使用 --release
参数运行 cargo build 或 cargo run 时 debug 标识会默认启用。
这里就相当于给你更详细的错误调用堆栈信息。
用 Result 处理可恢复的错误
大部分错误并没有严重到需要程序完全停止执行。JavaScript提供了try catch
来给我们捕获发生的错误。rust也提供了Result
枚举类型,帮助我们处理错误。
enum Result<T, E> {
Ok(T),
Err(E),
}
当我们打开一个文件的时候,我们就可以使用match
来匹配错误
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => panic!("Problem opening the file: {:?}", error),
};
}
这里我们告诉 Rust 当结果是 Ok
时,返回 Ok
成员中的 file
值,然后将这个文件句柄赋值给变量 greeting_file
。match
的另一个分支处理从 File::open
得到 Err
值的情况。在这种情况下,我们选择调用 panic!
宏。
注意与 Option
枚举一样,Result
枚举和其成员也被导入到了 prelude 中,所以就不需要在 match
分支中的 Ok
和 Err
之前指定 Result::
。
匹配不同的错误
上面File::open
是因为什么原因失败都会 panic!
。我们真正希望的是对不同的错误原因采取不同的行为:如果 File::open
因为文件不存在而失败,我们希望创建这个文件并返回新文件的句柄。如果 File::open
因为任何其他原因失败,例如没有打开文件的权限,我们仍然希望那样发生 panic!
。
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
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
操作可能导致的不同错误类型。
使用 unwrap_or_else
下面这段代码和上面的作用一样。但没有嵌套match
,unwrap_or_else
的作用在发生错误的时候执行后面的代码。没有直接返回成功的结果。
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Problem creating the file: {:?}", error);
})
} else {
panic!("Problem opening the file: {:?}", error);
}
});
}
失败时 panic 的简写:unwrap 和 expect
match
能够胜任它的工作,不过它可能有点冗长并且不总是能很好的表明其意图。
Result<T, E>
类型定义了很多辅助方法来处理各种情况。
-
其中之一叫做
unwrap
,如果Result
值是成员Ok
,unwrap
会返回Ok
中的值。如果Result
是成员Err
,unwrap
会为我们调用panic!
。 -
还有另一个类似于
unwrap
的方法它还允许我们选择panic!
的错误信息:expect
。使用expect
而不是unwrap
并提供一个好的错误信息可以表明你的意图并更易于追踪 panic 的根源。
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")
.expect("hello.txt should be included in this project");
}
unwrap
与expect
的区别就是,expect
可以自定义错误信息,unwrap
不可以
传播错误
当编写一个其实先会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。这被称为 传播(propagating)错误
这里展示了一个从文件中读取用户名的函数。如果文件不存在或不能读取,这个函数会将这些错误返回给调用它的代码:
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let username_file_result = File::open("hello.txt");
let mut username_file = match username_file_result {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut username = String::new();
match username_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}
}
就像在我们JavaScript中我们也会经常利用Promise
来返回一个reject
一个错误,再通过catch
捕获处理
传播错误的简写:? 运算符
上面我们使用match来匹配并返回了io::Error
,有时候也会觉得有些麻烦,所以rust提供了更简单的方法?
操作符号,可typescript中的?
可选链操作符,差不多只不过,rust会默认返回一个错误。
Result
值之后的 ?
与处理 Result
值的 match
表达式有着完全相同的工作方式。如果 Result
的值是 Ok
,这个表达式将会返回 Ok
中的值而程序将继续执行。如果值是 Err
,Err
中的值将作为整个函数的返回值,就好像使用了 return
关键字一样
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username_file = File::open("hello.txt")?;
let mut username = String::new();
username_file.read_to_string(&mut username)?;
Ok(username)
}
}
match
表达式与 ?
运算符所做的有一点不同:?
运算符所使用的错误值被传递给了 from
函数,它定义于标准库的 From
trait 中。
当 ?
运算符调用 from
函数时,收到的错误类型被转换为由当前函数返回类型所指定的错误类型。这在当函数返回单个错误类型来代表所有可能失败的方式时很有用,即使其可能会因很多种原因失败。
和typescript一样支持链式调用🤔,确实这样方便了很多。
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username = String::new();
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}
}
哪里可以使用 ? 运算符
?
运算符只能被用于返回值与 ?
作用的值相兼容的函数。因为 ?
运算符被定义为从函数中提早返回一个值
match
作用于一个 Result
值,提早返回的分支返回了一个 Err(e)
值。函数的返回值必须是 Result
才能与这个 return
相兼容。
main
函数一般都默认返回 ()
。但main
函数也可以返回 Result<(), E>
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let greeting_file = File::open("hello.txt")?;
Ok(())
}
Box<dyn Error>
类型是一个 trait 对象 后面会详细讲,这里你可以把它理解成,任意类型的错误类型,
什么时候使用panic!
要不要 panic!,在于你是否清楚的知道程序,是不是发生了不可恢复的错误,或者当前错误影响到了后续程序的执行,那我们就可以直接使用 panic!退出程序。
不然你可以返回Result
类型交给调用者处理该错误。
结语
Rust 的错误处理功能被设计为帮助你编写更加健壮的代码。panic!
宏代表一个程序无法处理的状态,并停止执行而不是使用无效或不正确的值继续处理。Rust 类型系统的 Result
枚举代表操作可能会在一种可以恢复的情况下失败。可以使用 Result
来告诉代码调用者他需要处理潜在的成功或失败。在适当的场景使用 panic!
和 Result
将会使你的代码在面对不可避免的错误时显得更加可靠。
这里对比起JavaScript的错误处理机制,感觉差不多,只不过JavaScript没有这么严格。大同小异,不是一个很难理解的点。