当前位置: 首页 > backend >正文

Rust 学习笔记:错误处理

Rust 学习笔记:错误处理

  • Rust 学习笔记:错误处理
    • 不可恢复的错误
    • 带有结果的可恢复错误
      • 匹配不同的错误
      • 出现错误时 panic 的快捷方式:unwrap 和 expect
      • 传播错误
      • 传播错误的快捷方式:? 操作符
      • 哪里可以使用 ? 操作符
    • panic or not panic,这是一个问题
      • 示例、原型代码和测试
      • 拥有比编译器更多信息的情况
      • 为验证创建自定义类型
      • 通过 from trait 返回自定义错误
      • 错误处理指南
    • 总结

Rust 学习笔记:错误处理

错误在软件中是不可避免的,所以 Rust 有很多特性可以处理出错的情况。

Rust 将错误分为两大类:可恢复的和不可恢复的错误。对于可恢复的错误,我们可能报告该错误并重试操作;对于不可恢复的错误,我们希望立即停止程序。

大多数语言不区分这两种错误,使用异常等机制进行处理。Rust 没有异常,类型为 Result<T, E> 表示可恢复的错误,而 panic! 宏在程序遇到不可恢复的错误时停止执行。

不可恢复的错误

在实践中,有两种方法可以引起 panic:

  • 采取导致代码 panic 的操作(例如访问超过结束的数组)
  • 显式调用 panic! 宏

默认情况下,panic 将打印一条失败消息,沿着堆栈返回并清理遇到的每个函数的数据,最后退出。该情况下,Rust 在 panic 发生时显示调用堆栈,以便更容易地跟踪 panic 的来源。

还有一种情况,在 Cargo.toml 中的 [profile] 部分中添加 panic = 'abort',设置在 panic 时立即中止,这将在不进行清理的情况下结束程序。

[profile.release]
panic = 'abort'

示例 1:

fn main() {panic!("crash and burn");
}

调用 panic 会导致最后两行中包含的错误消息,显示了我们的 panic 消息和源代码中发生 panic 的位置。

在这里插入图片描述
示例 2:

fn main() {let v = vec![1, 2, 3];v[99];
}

如果试图在一个不存在的索引上读取一个元素,Rust 将停止执行并 panic。

在这里插入图片描述

在 C 语言中,试图读取超出数据结构末端的数据是未定义的行为。你可能会得到内存中与数据结构中那个元素对应的位置,即使内存不属于那个结构。这被称为缓冲区过读,如果攻击者能够以这样一种方式操纵索引,从而读取存储在数据结构之后的不允许他们读取的数据,则可能导致安全漏洞。

在这里插入图片描述

Rust 还提醒我们:可以设置 RUST_BACKTRACE 环境变量(为任意不为 0 的值),以获得导致错误的确切回溯信息。

回溯是所有被调用的函数的列表。Rust 中的回溯和其他语言中的一样:从顶部开始读,直到看到自己写的文件,这就是问题产生的地方。该点上方的行是代码调用过的代码,下面几行代码调用了您的代码。

注意,这里使用 Windows 的 PowerShell 是无法运行的,可以切换 git bash 运行即可。

在这里插入图片描述

backtrace 的第 5 行指向项目中导致问题的行:src/main.rs 的第 4 行。如果我们不想让程序陷入恐慌,就应该从提到我们编写的文件的第一行所指向的位置开始调查。

根据操作系统和 Rust 版本的不同,backtrace 的确切输出可能会有所不同。

带有结果的可恢复错误

大多数错误并不严重到需要程序完全停止。有时,当一个函数失败时,它的原因很容易解释和响应。例如,如果您尝试打开一个文件,但由于该文件不存在而导致该操作失败,那么您可能希望创建该文件,而不是终止该进程。

Result 枚举被定义为有两个变体,Ok 和 Err,如下所示:

enum Result<T, E> {Ok(T),Err(E),
}

T 和 E 是泛型类型参数,T 表示成功情况下在 Ok 变量中返回的值的类型,而 E 表示失败情况下在 Err 变量中返回的错误的类型。我们可以在许多不同的情况下使用 Result 类型及其上定义的函数,其中我们希望返回的成功值和错误值可能不同。

让我们调用一个返回 Result 值的函数,因为该函数可能会失败:

use std::fs::File;fn main() {let greeting_file_result = File::open("hello.txt");
}

File::open 的返回类型是 Result<T, E>。泛型参数 T 是由 File::open 的实现用成功值 std::fs::File 的类型填充的,它是一个文件句柄。错误值中使用的 E 类型为 std::io:: error。File::open 函数需要有一种方法来告诉我们它是成功还是失败,同时给我们文件句柄或错误信息。这个信息正是 Result 枚举所传达的。

在 File::open 成功的情况下,变量 greeting_file_result 中的值将是一个包含文件句柄的 Ok 实例。在失败的情况下,greeting_file_result 中的值将是Err的一个实例,其中包含有关发生的错误类型的更多信息。

使用 match 能很好地处理 Result<T, E> 类型:

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:?}"),};
}

当结果为 Ok 时,这段代码将返回 Ok 变量的内部文件值,然后我们将该文件句柄值赋给变量 greeting_file。匹配之后,我们可以使用文件句柄进行读写。

当结果为 Err 时,我们选择调用 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:?}"),},_ => {panic!("Problem opening the file: {error:?}");}},};
}

File::open 在 Err 变量中返回的值的类型是 io::Error,它是标准库提供的结构体。这个结构体有一个方法 kind,我们可以调用它来获取 io::ErrorKind 的值。

枚举 io::ErrorKind 由标准库提供,具有代表 io 操作可能导致的不同类型错误的变体。我们想使用的变体是 ErrorKind::NotFound,它表示我们试图打开的文件还不存在。这种情况下,我们尝试用 file::create 创建文件。但是,由于 File::create 也可能失败,我们还需要在内部匹配表达式中添加另一个 match:创建成功时,同样返回文件句柄;无法创建时,打印一条不同的错误消息。

出现错误时 panic 的快捷方式:unwrap 和 expect

使用 match 可能有点冗长,并且不能很好地传达意图。Result<T, E> 类型在其上定义了许多帮助器方法来执行各种更具体的任务。

unwrap 方法是一个快捷方法,如果 Result 值是 Ok 变体,则 unwrap 将返回 Ok 内部的值;如果 Result 是 Err 变体,unwrap 将调用 panic! 宏。

use std::fs::File;fn main() {let greeting_file = File::open("hello.txt").unwrap();
}

如果我们在没有 hello.txt 文件的情况下运行这段代码,我们将看到 panic 发出的错误消息:

在这里插入图片描述

使用 expect 可以自定义 panic 错误信息,并使跟踪恐慌的来源更容易。

expect 的语法是这样的:

use std::fs::File;fn main() {let greeting_file = File::open("hello.txt").expect("hello.txt should be included in this project");
}

我们使用 expect 的方式与 unwrap 相同:返回文件句柄或调用 panic! 宏。expect 在调用 panic! 宏时打印我们传递的参数。

在这里插入图片描述

unwrap 更常用,因为可以在调试中使用更多的信息。

传播错误

当函数的实现调用可能失败的东西时,可以将错误返回给调用代码。这被称为传播错误,并为调用代码提供了更多的控制。

示例:

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),}
}

让我们先看看函数的返回类型:Result<String, io::Error>。这意味着函数返回一个 Result<T, E> 类型的值,其中泛型参数 T 已经用具体类型 String 填充,泛型类型E已经用具体类型 io::Error 填充。

如果这个函数成功,调用这个函数的代码将收到一个 Ok 值,该值包含一个 String——这个函数从文件中读取的用户名。如果此函数遇到任何问题,调用代码将接收一个 Err 值,该值包含 io::Error 的实例,该实例包含有关问题所在的更多信息。

之所以选择 io::Error 作为函数的返回类型,是因为在函数体中调用的 File::open 函数和 read_to_string 方法这两个可能失败的操作返回的错误值的类型恰好是 io::Error。

这种传播错误的模式在 Rust 中非常常见,因此 Rust 为了方便起见提供了问号操作符 ? 。

传播错误的快捷方式:? 操作符

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)
}

? 放置在 Result 值之后,其工作方式与之前为处理 Result 值而定义的匹配表达式几乎相同。如果 Result 的值为Ok,则该表达式将返回 Ok 中的值,程序将继续执行。如果该值为 Err,则整个函数将返回 Err,就像我们使用了 return 关键字一样,因此错误值将传播到调用代码。

? 操作符消除了大量的样板文件,使这个函数的实现更简单。我们甚至可以通过在 ? 之后立即链接方法调用来进一步缩短代码:

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)
}

代码还能更短吗?当然可以!

将文件读入字符串是一种相当常见的操作,因此标准库提供了方便的 fs::read_to_string 函数,该函数打开文件,创建一个新的 String,读取文件的内容,将内容放入该 String并返回。

最终,函数被简化成了 one line code:

use std::fs;
use std::io;fn read_username_from_file() -> Result<String, io::Error> {fs::read_to_string("hello.txt")
}

哪里可以使用 ? 操作符

? 操作符只能在返回类型与使用 ? 的值兼容的函数中使用,即 Result 类型。

让我们看看一个错误示例:

use std::fs::File;fn main() {let greeting_file = File::open("hello.txt")?;
}

在这里插入图片描述

这段代码打开一个文件,可能会失败。? 操作符在 File::open 返回的 Result 值之后,但是这个主函数的返回类型是 (),而不是 Result。

这个错误指出我们只允许使用 ? 返回 Result、Option 或其他实现 FromResidual 的类型的函数中的操作符。

要修复这个错误,有两种选择。一种选择是更改函数的返回类型,使其与使用的值兼容。只要没有限制,就继续操作。另一种选择是使用 match 或 Result<T, E> 方法之一,以任何合适的方式处理 Result<T, E>。

? 操作符在Option<T>上调用时的行为与在 Result<T, E> 上调用时的行为相似:如果值为 None,则在该点将提前从函数返回 None。如果值是 Some,则 Some 内的值是表达式的结果值,函数继续执行。

下面给出一个示例:

fn last_char_of_first_line(text: &str) -> Option<char> {text.lines().next()?.chars().last()
}

这个函数返回 Option<char>,因为这里可能有字符,但也可能没有。

如果 text 不是空字符串,next 将返回一个 Some 值,其中包含 text 中第一行的字符串切片。? 提取字符串切片,然后调用该字符串切片上的 chars 来获取其字符的迭代器,调用 last 来返回迭代器中的最后一项。

注意,可以使用 ? 操作符对返回 Result 的函数中的 Result 进行操作,也可以使用 ? 操作符在返回 Option 的函数中对 Option 进行操作,但不能混合匹配。? 操作符不会自动将结果转换为选项,反之亦然。在这些情况下,可以使用 Result 上的 ok 方法或 Option 上的 ok_or 方法等方法显式地进行转换。

到目前为止,我们使用的所有 main函数都是 return ()。main 函数的特殊之处在于它是可执行程序的入口点和出口点,它的返回类型是有限制的,这样程序才能按照预期的方式运行。

幸运的是,main 函数也可以返回 Result<(), E>。我们将 main 函数的返回类型更改为 Result<(), Box<dyn Error>>,并在末尾添加返回值 Ok(())。这段代码现在可以编译了。

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 对象,表示“任何类型的错误”。使用 ? 允许在错误类型为 Box<dyn error> 的主函数中返回 Result 值,因为它允许提前返回任何 Err 值,即使在 main 函数体中添加更多返回其他错误的代码,这个签名仍然是正确的。

当 main 函数返回 Result<(), E> 时,如果 main 函数返回 Ok(()),可执行程序将以 0 的值退出;如果 main 函数返回 Err 值,可执行程序将以非 0 的值退出。这与用 C 编写的可执行程序的约定兼容。

main 函数可以返回任何实现 std::process::Termination trait 的类型,它包含一个返回 ExitCode 的函数报告。

panic or not panic,这是一个问题

本节将总结一些关于如何决定是否在库代码中 panic 的一般指导原则。

示例、原型代码和测试

当编写示例来说明某些概念时,使用 panic 可以使得示例变得清晰。

类似地,在决定如何处理错误之前,unwrap 和 expect 方法在原型制作时非常方便,它们会在代码中留下清晰的标记。

如果一个方法调用在测试中失败,希望整个测试都失败,用 panic 比较好。

拥有比编译器更多信息的情况

当代码确保 Result 将具有 Ok 值时,调用 unwrap 或 expect 也是合适的,但仍然有一个需要处理的 Err 变体。

示例:

    use std::net::IpAddr;let home: IpAddr = "127.0.0.1".parse().expect("Hardcoded IP address should be valid");

我们肯定希望以更健壮的方式处理结果。except 中处理了Result 为 Err 的情况。

为验证创建自定义类型

让我们进一步考虑使用 Rust 的类型系统来确保我们有一个有效的值,并看看如何创建一个用于验证的自定义类型。

完善之前的猜数游戏,引导用户进行有效的猜测,并在用户猜测超出范围的数字时与用户键入(例如字母)时具有不同的行为。

    loop {// --snip--let guess: i32 = match guess.trim().parse() {Ok(num) => num,Err(_) => continue,};if guess < 1 || guess > 100 {println!("The secret number will be between 1 and 100.");continue;}match guess.cmp(&secret_number) {// --snip--}

if 表达式检查我们的值是否超出了范围,之后继续进行 guess 和秘密数之间的比较。然而,这不是一个理想的解决方案:如果程序只对 1 到 100 之间的值进行操作是绝对关键的,并且它有许多具有此需求的函数,那么在每个函数中进行这样的检查将是乏味的(并且可能影响性能)。

相反,我们可以在专用模块中创建新类型,并将验证放入函数中以创建该类型的实例,而不是到处重复验证。这样,函数就可以安全地在其签名中使用新类型,并放心地使用它们接收到的值。我们定义 Guess 类型的一种方法,该方法只在新函数接收到 1 到 100 之间的值时创建实例。

pub struct Guess {value: i32,
}impl Guess {pub fn new(value: i32) -> Guess {if value < 1 || value > 100 {panic!("Guess value must be between 1 and 100, got {value}.");}Guess { value }}pub fn value(&self) -> i32 {self.value}
}

首先,我们创建一个名为 guessing_game 的新模块。接下来,我们在该模块中定义了一个名为 Guess 的结构体,该结构体有一个 i32 类型、名为 value 的字段,这是存储数字的地方。

然后,我们在 Guess 上实现一个名为 new 的关联函数,该函数创建 Guess 值的实例。这个新函数被定义为有一个名为 value 的参数,类型为 i32,并返回 Guess。代码测试 value 以确保它在 1 到 100 之间。如果 value 没有通过这个测试,就会 panic。

接下来,我们实现一个名为 value 的方法,它借用 self,没有任何其他参数,并返回一个 i32。这种方法有时被称为 getter,因为它的目的是从字段中获取一些数据并返回这些数据。这个公共方法是必要的,因为 Guess 结构体的 value 字段是私有的。

如果一个函数有一个参数或者只返回 1 到 100 之间的数字,那么它就可以在它的签名中声明它接受或返回一个 Guess 而不是一个 i32,并且不需要在函数体中做任何额外的检查。

通过 from trait 返回自定义错误

这里将 io::Error 和 ParseIntError 通过 from trait 转换成枚举 MyError。

use std::{fs, io};
use std::fs::File;
use std::io::{read_to_string, Read};
use std::num::ParseIntError;#[derive(Debug)]
pub enum MyError {Io(io::Error),ParseInt(ParseIntError),Other(String),
}impl From<io::Error> for MyError {fn from(err: io::Error) -> Self {MyError::Io(err)}
}impl From<ParseIntError> for MyError {fn from(err: ParseIntError) -> Self {MyError::ParseInt(err)}
}fn read_username_from_file() -> Result<String, MyError> {let mut name = String::new();let file = File::open("username.txt")?.read_to_string(&mut name)?;let num: i32 = "55".parse()?;Ok(name)
}fn main() {}

错误处理指南

当您的代码可能以糟糕的状态结束时,建议让代码陷入 panic。坏状态包含:

  • 无效值、矛盾值或丢失值被传递给代码
  • 用户以错误的格式输入数据

然而,当失败在所难免时,返回 Result 比制造 panic 更合适。

总结

Rust 的错误处理特性旨在帮助编写更健壮的代码。

panic! 宏表示程序处于无法处理的状态,并将进程停止,而不是尝试使用无效或不正确的值继续。

枚举 Result 使用 Rust 的类型系统来指示操作可能失败的方式,代码可以从中恢复。

何时使用 panic:不可恢复的错误场景

  • 程序进入不可预期的 bad state
  • 安全问题
  • 代码不能继续执行
  • 违反函数契约或关键假设

何时使用 Result:可能恢复的错误场景

  • 提供恢复选项
  • 预期可能发生的错误
  • 希望调用者决定如何处理错误

推荐使用 panic 的情况:

  • 原型代码、示例、测试
  • 安全性关键的输入验证
  • 调用外部不可控代码时的异常状态

推荐使用 Result 的情况:

  • 处理可预期的错误
  • HTTP 请求失败
  • 解析错误
  • 用户输入验证

更多资料参见 Rust Book: Error Handling - To panic! or not to panic!

http://www.xdnf.cn/news/7352.html

相关文章:

  • Web 技术与 Nginx 网站环境部署
  • Pycharm 选择Python Interpreter
  • 酒水饮料批发零售商城小程序开发
  • 深入浅出程序设计竞赛(洛谷基础篇) 第十三章 二分查找与二分答案
  • 小米MUJIA智能音频眼镜来袭
  • 如何查看 Ubuntu开机是否需要密码
  • 一键启动多个 Chrome 实例并自动清理的 Bash 脚本分享!
  • 视图+触发器+临时表+派生表
  • 使用Docker部署React应用与Nginx
  • 分组背包问题:如何最大化背包价值?
  • 基于Java在高德地图面查询检索中使用WGS84坐标的一种方法-以某商场的POI数据检索为例
  • 常见提示词攻击方法和防御手段——提示词越狱
  • 专题五:floodfill算法(太平洋大西洋水流问题)
  • 前端加载超大图片(100M以上)实现秒开解决方案
  • 【分治】快速排序
  • lowcoder数据库操作4:显示查询数据表格
  • 加载渲染geojson数据
  • list.forEach(s -> countService.refreshArticleStatisticInfo(s.getId())); 讲解一下语法
  • Blender cycles烘焙贴图笔记
  • Linux 文件(2)
  • JavaScript 中的五种继承方式进行深入对比
  • vue3 vite 项目中自动导入图片
  • Axure疑难杂症:垂直菜单展开与收回(4大核心问题与专家级解决方案)
  • 新能源汽车充电桩管理平台如何利用智慧技术优化资源配置问题?
  • Triton介绍和各平台支持情况分析
  • Spring 代理与 Redis 分布式锁冲突:一次锁释放异常的分析与解决
  • 每日c/c++题 备战蓝桥杯(洛谷P4715 【深基16.例1】淘汰赛 题解)
  • 基于Zynq SDK的LWIP UDP组播开发实战指南
  • redis的List为什么用ziplist和quicklist
  • SCGI 服务器详解