✨✨ 欢迎大家来到景天科技苑✨✨
🎈🎈 养成好习惯,先赞后看哦~🎈🎈
🏆 作者简介:景天科技苑
🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。
🏆《博客》:Rust开发,Python全栈,Golang开发,云原生开发,PyQt5和Tkinter桌面开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi,flask等框架,云原生K8S,linux,shell脚本等实操经验,网站搭建,数据库等分享。所属的专栏:Rust语言通关之路
景天的主页:景天科技苑
文章目录
Rust错误处理
在 Rust 中,错误处理是一个核心特性,它通过 Result 和 Option 类型以及 panic! 机制来管理错误和异常情况。Rust 的错误处理强调显式性和安全性,避免了传统异常机制的隐式控制流问题。
Rust 主要有两种错误处理方式:可恢复错误和不可恢复错误。
可恢复错误 (Recoverable Errors)
使用 Result<T, E>、Option 类型表示
适用于预期可能发生的错误情况(如文件未找到、网络连接失败等)
由调用者决定如何处理
不可恢复错误 (Unrecoverable Errors)
使用 panic! 宏触发
适用于程序遇到无法继续执行的严重错误
会导致线程立即终止,默认情况下也会终止整个程序
1、可恢复错误
可恢复错误通常代表向用户报告错误和重试操作是合理的情况,例如文件未找到。
1.1 Result
Rusult中使用Result<T, E>来实现。
Result 是一个 枚举:
enum Result<T, E> {
Ok(T),
Err(E),
}
Ok(T):成功时,返回一个值 T
Err(E):失败时,返回一个错误值 E
实战案例:
use std::io;
use std::fs;
//函数返回Result
fn read_file(path: &str) -> Result<String, io::Error> {
fs::read_to_string(path)
}
fn main() {
match read_file("hello.txt") {
Ok(content) => println!("File content: {}", content),
Err(e) => println!("Error reading file: {}", e),
}
}
如果文件存在
就正常读取文件内容
1.2 Option
Option<T>
表示一个值可能存在(Some(T))或不存在(None),适用于可能返回空值的情况:
fn divide(a: f64, b: f64) -> Option<f64> {
if b == 0.0 {
None
} else {
Some(a / b)
}
}
fn main() {
//使用if let语法
if let Some(result) = divide(10.0, 2.0) {
println!("Result: {}", result);
} else {
println!("Cannot divide by zero!");
}
}
2、不可恢复错误panic!
2.1 panic
当发生严重错误时(如数组越界、逻辑错误),可以直接调用 panic!,程序会停止运行并打印错误信息。
Panic 中的栈展开与终止
当出现 panic! 时,程序默认会开始 展开(unwinding),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。
另一种选择是直接 终止(abort),这会不清理数据就退出程序。
那么程序所使用的内存需要由操作系统来清理。
如果你需要项目的最终二进制文件越小越好,可以由 panic 时展开切换为终止,通过在 Cargo.toml 的 [profile] 部分增加 panic = ‘abort’ 。
例如,如果你想要在发布模式中 panic 时直接终止:
[profile.release]
panic = ‘abort’
panic!使用案例:
fn main() {
panic!("Something went wrong!");
}
使用 panic! 的 backtrace
我们可以设置 RUST_BACKTRACE 环境变量来得到一个 backtrace backtrace 是一个执行到目前位置所有被调用的函数的列表。
在程序调试时,比较有用
Rust 的 backtrace 跟其他语言中的一样:阅读 backtrace 的关键是从头开始读直到发现你编写的文件。
这就是问题的发源地。这一行往上是你的代码调用的代码;往下则是调用你的代码的代码。
这些行可能包含核心 Rust 代码,标准库代码或用到的 crate 代码。
默认RUST_BACKTRACE为0
如果要运行时加上调用信息,只需要在运行的环境变量中加上 RUST_BACKTRACE=1
Windows Powershell终端设置临时环境变量
$env:RUST_BACKTRACE=1
然后再运行 cargo run
可以看到调用函数列表
当设置 RUST_BACKTRACE 环境变量时 panic! 调用所生成的 backtrace 信息
2.2 unwrap 和 expect
失败时 panic 的简写: unwrap 和 expect
match 能够胜任它的工作,不过它可能有点冗长并且不总是能很好的表明其意图。
Result<T, E> 类型定义了很多辅助方法来处理各种情况。其中之一叫做 unwrap ,它的实现就类似于 match 语句。
unwrap:
如果 Result 值是成员Ok , unwrap 会返回 Ok 中的值。如果 Result 是成员 Err , unwrap 会为我们调用 panic! 。
注意:该方法并不安全
unwrap()是 Result 和 0ption 类型提供的方法之一。它是一个简便的方法,用于获取 0k或 Some 的值,如果是 Err 或 None 则会引发panic
use std::io;
use std::fs;
//函数返回Result
fn read_file(path: &str) -> Result<String, io::Error> {
fs::read_to_string(path)
}
fn main() {
//使用unwrap简写
let content = read_file("hello.txt").unwrap();
println!("File content: {}", content);
}
expect:
使用 expect 可以提供一个好的错误信息,可以表明你的意图并更易于追踪 panic 的根源。
expect 与 unwrap 的使用方式一样:返回文件句柄或调用 panic! 宏。
expect 用来调用 panic! 的错误信息将会作为参数传递给 expect ,而不像 unwrap 那样使用默认的 panic! 信息。
use std::io;
use std::fs;
//函数返回Result
fn read_file(path: &str) -> Result<String, io::Error> {
fs::read_to_string(path)
}
fn main() {
//使用expect简写,可以指定报错信息
let content = read_file("hello.txt").expect("Failed to read file");
println!("File content: {}", content);
}
因为这个错误信息以我们指定的文本开始, Failed to open file ,将会更容易找到代码中的错误信息来自何处。
如果在多处使用 unwrap ,则需要花更多的时间来分析到底是哪一个 unwrap 造成了 panic,因为所有的 unwrap 调用都打印相同的信息。
3、传播错误
当编写一个函数,但是该函数可能会失败,此时除了在这个函数中处理错误外,还可以将错误传递给调用者,让调用者知道这个错误并决定该如何处理。
这被称为 传播(propagating)错误,这样能更好的控制代码调用,因为比起你代码所拥有的上下文,调用者可能拥有更多信息或逻辑来决定应该如何处理错误。
3.1 常规错误传播
如下展示了一个从文件中读取用户名的函数。如果文件不存在或不能读取,这个函数会将这些错误返回给调用它的代码:
use std::io;
use std::io::Read;
use std::fs::File;
//函数返回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),
}
}
fn main() {
match read_username_from_file() {
Ok(s) => println!("File content: {}", s),
Err(e) => println!("Error reading file: {}", e),
}
}
首先让我们看看函数的返回值: Result<String, io::Error> 。这意味着函数返回一个 Result<T, E> 类型的值,其中泛型参数 T 的具体类型是 String ,而 E 的具体类型是 io::Error 。
如果这个函数没有出任何错误成功返回,函数的调用者会收到一个包含 String 的 Ok 值————函数从文件中读取到的用户名。
如果函数遇到任何错误,函数的调用者会收到一个 Err 值,它储存了一个包含更多这个问题相关信息的 io::Error 实例。
这里选择 io::Error 作为函数的返回值是因为它正好是函数体中那两个可能会失败的操作的错误返回值: File::open 函数和 read_to_string 方法。
函数体以 File::open 函数开头。接着使用 match 处理返回值 Result ,当Err 时不再调用 panic! ,而是提早返回并将 File::open 返回的错误值作为函数的错误返回值传递给调用者。
如果File::open 成功了,我们将文件句柄储存在变量 f 中并继续。
接着我们在变量 s 中创建了一个新 String 并调用文件句柄 f 的 read_to_string 方法来将文件的内容读取到 s 中。
read_to_string 方法也返回一个 Result 因为它也可能会失败:哪怕是 File::open 已经成功了。
所以我们需要另一个 match 来处理这个 Result :如果 read_to_string 成功了,那么这个函数就成功了,并返回文件中的用户名,
它现在位于被封装进 Ok 的 s 中。如果 read_to_string 失败了,则像之前处理 File::open 的返回值的 match 那样返回错误值。
不过并不需要显式的调用 return ,因为这是函数的最后一个表达式。
调用这个函数的代码最终会得到一个包含用户名的 Ok 值,或者一个包含 io::Error 的 Err 值。
我们无从得知调用者会如何处理这些值。
例如,如果他们得到了一个 Err 值,他们可能会选择 panic! 并使程序崩溃、使用一个默认的用户名或者从文件之外的地方寻找用户名。
我们没有足够的信息知晓调用者具体会如何尝试,所以将所有的成功或失败信息向上传播,让他们选择合适的处理方法。
这种传播错误的模式在 Rust 是如此的常见,以至于有一个更简便的专用语法: ? 。
3.2 传播错误的简写: ?
在可能出错的语句之后加上?。来抛出可能出现的error
? 用于简化 Result 或 0ption 类型的错误传播。
它只能用于返回 Result 或0ption 的函数中,并且在函数内部可以像使用 unwrap()一样访问 Ok或 Some的值,但是如果是 Err 或 None 则会提前返回
如果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 f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
fn main() {
//调用read_username_from_file函数
//如果函数返回的是Ok,则打印出结果
//如果函数返回的是Err,则打印出错误信息
match read_username_from_file() {
Ok(s) => println!("username: {}", s),
Err(e) => println!("Error: {}", e),
}
}
Result 值之后的 ? 被定义为与上面案例 中定义的处理 Result 值的 match 表达式有着完全相同的工作方式。
如果Result 的值是 Ok ,这个表达式将会返回 Ok 中的值而程序将继续执行。
如果值是 Err , Err 中的值将作为整个函数的返回值,就好像使用了 return 关键字一样,这样错误值就被传播给了调用者。
跟上面的 match 表达式与问号运算符所做的有一点不同: ? 所使用的错误值被传递给了 from 函数,它定义于标准库的 From trait 中,其用来将错误从一种类型转换为另一种类型。
到问号运算符调用 from 函数时,收到的错误类型被转换为定义为当前函数返回的错误类型。
这在当一个函数返回一个错误类型来代表所有可能失败的方式时很有用,即使其可能会因很多种原因失败。
只要每一个错误类型都实现了 from 函数来定义如将其转换为返回的错误类型,问号运算符会自动处理这些转换。
File::open 调用结尾的 ? 将会把 Ok 中的值返回给变量 f 。如果出现了错误, ? 会提早返回整个函数并将一些 Err 值传播给调用者。同理也适用于 read_to_string 调用结尾的 ? 。
3.3 ? 链式传播
? 消除了大量样板代码并使得函数的实现更简单。我们甚至可以在 ? 之后直接使用链式方法调用来进一步缩短代码
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)
}
fn main() {
//调用read_username_from_file函数
//如果函数返回的是Ok,则打印出结果
//如果函数返回的是Err,则打印出错误信息
match read_username_from_file() {
Ok(s) => println!("username: {}", s),
Err(e) => println!("Error: {}", e),
}
}
在 s 中创建新的 String 被放到了函数开头;这一部分没有变化。我们对 File::open(“hello.txt”)? 的结果直接链式调用了 read_to_string ,而不再创建变量 f 。
仍然需要 read_to_string 调用结尾的 ? ,而且当 File::open 和read_to_string 都成功没有失败时返回包含用户名 s 的 Ok 值。这是一个与众不同且更符合工程学的写法。
? 只能被用于返回 Result 或Option的函数
? 只能被用于返回值类型为 Result或Option 的函数。
match 的 return Err(e) 部分要求返回值类型是 Result ,所以函数的返回值必须是 Result 才能与这个 return 相兼容。
让我们看看在 main 函数中使用 ? 会发生什么,如果你还记得的话其返回值类型是 () :
use std::fs::File;
fn main() {
let f = File::open("hello.txt")?;
}
错误指出只能在返回 Result或者Option 的函数中使用问号运算符。
在不返回 Result 或者Option的函数中,当调用其他返回 Result 的函数时,需要使用 match 或 Result 的方法之一来处理,而不能用 ? 将潜在的错误传播给调用者。
如果非要在main中使用,可以让main函数返回一个Result或者Option。错误用Box智能指针来实现
Result<(), Box<dyn std::error::Error>>
4、自定义错误类型
自定义Error类型的三个步骤
- 定义错误类型结构体: 创建一个结构体来表示你的错误类型,通常包含一些字段来描述错误的详细信息。
- 实现 std::fmt::Display trait: 实现这个 trait 以定义如何展示错误信息。这是为了使错误能够以人类可读的方式打印出来。
- 实现 std::error::Error trait:实现这个 trait 以满足 Rust 的错误处理机制的要求。
use std::fmt;
#[derive(Debug)]
//自定义结构体,定义错误类型
enum MyError {
Io(std::io::Error),
Parse(std::num::ParseIntError),
}
//实现std::fmt::Display trait
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MyError::Io(e) => write!(f, "IO error: {}", e),
MyError::Parse(e) => write!(f, "Parse error: {}", e),
}
}
}
//实现std::error::Error trait。这个不重写也没关系,有默认的
impl std::error::Error for MyError {}
//读取文件并解析为整数
//返回Result类型,MyError就是自定义的错误类型
fn read_and_parse() -> Result<i32, MyError> {
let num = std::fs::read_to_string("number.txt").map_err(MyError::Io)?;
num.trim().parse::<i32>().map_err(MyError::Parse)
}
fn main() {
match read_and_parse() {
Ok(num) => println!("Parsed number: {}", num),
Err(e) => println!("Error: {}", e),
}
}
5、什么时候用panic,什么时候用Result?
那么,该如何决定何时应该 panic! 以及何时应该返回 Result 呢?如果代码 panic,就没有恢复的可能。
你可以选择对任何错误场景都调用 panic! ,不管是否有可能恢复,不过这样就是你代替调用者决定了这是不可恢复的。
选择返回Result 值的话,就将选择权交给了调用者,而不是代替他们做出决定。
调用者可能会选择以符合他们场景的方式尝试恢复,或者也可能干脆就认为 Err 是不可恢复的,所以他们也可能会调用 panic! 并将可恢复的错误变成了不可恢复的错误。
因此返回 Result 是定义可能会失败的函数的一个好的默认选择。
有一些情况 panic 比返回 Result 更为合适,不过他们并不常见。在示例、代码原型和测试中,以及那些人们认为不会失败而编译器不这么看的情况下, panic 是合适的。
示例、代码原型和测试都非常适合 panic、unwrap、expect
当你编写一个示例来展示一些概念时,在拥有健壮的错误处理代码的同时也会使得例子不那么明确。
例如,调用一个类似 unwrap 这样可能 panic! 的方法可以被理解为一个你实际希望程序处理错误方式的占位符,它根据其余代码运行方式可能会各不相同。
类似的, unwrap 和 expect 方法在原型设计时非常方便,在你决定该如何处理错误之前。他们在代码中留下了明显的记
号,以便你准备使程序变得更健壮时作为参考。
如果方法调用在测试中失败了,我们希望这个测试都失败,即便这个方法并不是需要测试的功能。因为 panic! 是测试
如何被标记为失败的,调用 unwrap 或 expect 都是非常有道理的。
当你比编译器知道更多的情况适合panic
当你有一些其他的逻辑来确保 Result 会是 Ok 值的时候调用 unwrap 也是合适的,虽然编译器无法理解这种逻辑。仍
然会有一个 Result 值等着你处理:总的来说你调用的任何操作都有失败的可能性,即便在特定情况下逻辑上是不可能
的。如果通过人工检查代码来确保永远也不会出现 Err 值,那么调用 unwrap 也是完全可以接受的,
生产项目中,应该使用Result
6、第三方错误库
对于更复杂的错误处理,可以使用以下库:
anyhow: 适用于应用程序的错误处理,简化错误类型
thiserror: 适用于库的错误处理,可以方便地派生自定义错误类型
anyhow 示例
use anyhow::{Context, Result};
fn main() -> Result<()> {
let path = "test.txt";
let data = std::fs::read(path)
.with_context(|| format!("Failed to read file: {}", path))?;
println!("{}", data.len());
Ok(())
}
thiserror 示例
use thiserror::Error;
#[derive(Error, Debug)]
enum MyError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Parse error: {0}")]
Parse(#[from] std::num::ParseIntError),
}
fn read_and_parse() -> Result<i32, MyError> {
let num = std::fs::read_to_string("number.txt")?;
Ok(num.trim().parse()?)
}
7、总结
Rust 的错误处理功能被设计为帮助你编写更加健壮的代码。 panic! 宏代表一个程序无法处理的状态,并停止执行而不是
使用无效或不正确的值继续处理。Rust 类型系统的 Result 枚举代表操作可能会在一种可以恢复的情况下失败。可以使
用 Result 来告诉代码调用者他需要处理潜在的成功或失败。在适当的场景使用 panic! 和 Result 将会使你的代码在面
对无处不在的错误时显得更加可靠。