Ice 的博客

用心记录思想

带有结果的可恢复错误

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

我是 Ice,一名程序员。

主业做后端开发,长期和 Python 打交道;业余折腾 Rust、AI 工具和各类效率小玩意。

这里主要记录:

  • Rust 学习笔记
  • AI 工具的折腾与使用实录
  • 一些随想和读书笔记

联系方式:

本站使用 Hexo + NexT 搭建。

使用panic!的backtrace

让我们来看看另外一个例子,当因为我们自己的代码有bug而导致库函数调用panic!是什么样的。如Listing 9-1所示的代码尝试访问超出范围的vector的索引。

Filename: src/main.rs

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

v[99];
}

Listing 9-1: Attempting to access an element beyond the end of a vector, which will cause a call to panic!

这里,我们尝试去访问第100个元素(索引99就是第100个元素,索引是从0开始的),但是vector只有3个元素,肯定会报错。在这个场景中,Rust就会panic。使用[]就是企图返回一个元素,但是你传入一个无效的索引,又不会有这个元素,Rust就肯定不会返回正确的结果。

在C语言中,尝试读取数据结构之后的值是未定义行为(undefined behavior)。你可能会得到一个这个位置的对应的值,但是这个可能是任何的值,但就不是你想要访问的值,因为这个位置的值不属于你想要访问的数据结构范围的值。这个被叫做缓冲区溢出(buffer overread),并且可能会导致安全漏洞,比如攻击者可以像这样操作索引来读取存储在数据结构之后不被允许访问的数据。

为了防止这种漏洞,如果尝试读取一个索引不存在的元素,Rust会停止执行并拒绝继续。尝试运行上面的程序会出现如下的错误:

1
2
3
4
5
6
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished dev [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/panic`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

上面的错误信息指出错误发生在src/main.rs文件的第4行第5个字符,我们尝试访问索引为99的元素。接着一行的信息告诉我们可以设置RUST_BACKTRACE环境变量来获取backtrace查看发生了什么错误。一个backtrace是一个已经被调用直到当前这一个点的所有的函数列表。Backtrace在Rust的工作原理和其他语言一样:阅读backtrace的关键是从头开始读一直到发现你编写的代码;往下则是你调用的代码。这些行可能包含核心Rust代码,标准库代码或用到的crate代码。让我们设置RUST_BACKTRACE环境变量在Listing 9-2中看看都输出了些什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ RUST_BACKTRACE=1 cargo run
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
stack backtrace:
0: rust_begin_unwind
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/std/src/panicking.rs:584:5
1: core::panicking::panic_fmt
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:142:14
2: core::panicking::panic_bounds_check
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:84:5
3: <usize as core::slice::index::SliceIndex<[T]>>::index
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:242:10
4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:18:9
5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/alloc/src/vec/mod.rs:2591:9
6: panic::main
at ./src/main.rs:4:5
7: core::ops::function::FnOnce::call_once
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/ops/function.rs:248:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

Listing 9-2: The backtrace generated by a panic!displayed when the environment variable RUST_BACKTRACEis set

真是输出了一大坨啊!操作系统不同,Rust版本不同,你看到的输出可能会有所不同。为了使用这些信息进行回溯,必须启用debug模式。默认就是debug模式,只要在cargo build或者cargo run之后不要加--release参数。

在Listing 9-2的第6行的backtrace指出了我们项目造成的问题:src/main.rs的第4行。如果我们不想我们的程序panic,我们就应该开始检查定位到第一行我们自己写的代码。在Listing 9-1中,我们故意写了造成panic的代码,修复这个panic就是不要去访问超出vector范围的索引。当你的代码在将来panic, 你需要弄清楚代码使用什么数据做了什么样的操作,以及正确情况下代码应该做什么。

本章后面的小节 “To panic! Or not to panic! 部分讲回panic!,详细讲解应该或者不应该用panic!来处理错误情况。接下来一节我们来讲解如何使用Result恢复一个错误。

错误处理

在软件中错误是很常见的事实。当有时候软件运行错误的时候,Rust有很多处理这些场景的特性。在很多情况下,Rust要求你确认每一个错误发生的可能,并在代码编译前就采取相应的行为。这个必要前提会让你的程序更加健壮:确保你会提前发现错误,并处理得当,在你将代码部署到生产环境之前。

Rust将错误分为两大类:可恢复的(recoverable)和不可恢复的(unrecoverable)。对于可恢复错误,比如文件未找打错误,我们很可能只是想向用户报告问题并重试操作。不可恢复错误总是系统性bugs,像尝试访问一个超过数组最尾端的位置,此时,我们就会想要程序立刻停止运行。

大多数编程语言无法区分这两种错误,并且都使用同样的方式,使用如exceptions机制。Rust没有exceptions。而是有一个Result<T, E>的类型来处理可恢复错误。另外,当程序发生一个不可恢复的错误,Rust用panic!宏来中止运行程序。

这一章,首次讨论panic!宏,然后讨论有关Result<T, E>返回的值。

另外,我们还要探索如何决定一个错误应该是恢复或者是停止运行。

总结

vector、字符串和哈希 map 会在你的程序需要储存、访问和修改数据时帮助你。这里有一些你应该能够解决的练习问题:

  • 给定一系列数字,使用 vector 并返回这个列表的中位数(排列数组后位于中间的值)和众数(mode,出现次数最多的值;这里哈希 map 会很有帮助)。
  • 将字符串转换为 Pig Latin,也就是每一个单词的第一个辅音字母被移动到单词的结尾并增加 “ay”,所以 “first” 会变成 “irst-fay”。元音字母开头的单词则在结尾增加 “hay”(“apple” 会变成 “apple-hay”)。牢记 UTF-8 编码!
  • 使用哈希 map 和 vector,创建一个文本接口来允许用户向公司的部门中增加员工的名字。例如,“Add Sally to Engineering” 或 “Add Amir to Sales”。接着让用户获取一个部门的所有员工的列表,或者公司每个部门的所有员工按照字典序排列的列表。

标准库 API 文档中描述的这些类型的方法将有助于你进行这些练习!

我们已经开始接触可能会有失败操作的复杂程序了,这也意味着接下来是一个了解错误处理的绝佳时机!

字符串并不简单

总之,字符串是很复杂的。不同的语言会有不同的选择如何将这种复杂性呈现给程序员。Rust选择将String数据的正确处理作为所有Rust程序的默认行为,这意味着程序员必须在处理UTF-8数据上花更多的心思。这个权衡在其他编程语言中暴露了更多字符串的复杂性,但它可以防止你在开发生命周期的后期处理涉及非ASCII字符的错误。
好消息是标准库基于String和&str类型提供了许多功能来正确处理这些复杂的情况。确保查看文档中有用的方法,比如在字符串中搜索的contains和用另一个字符串替换字符串的一部分replace。
让我们切换到略微简单一点的内容:哈希映射!

丢弃vector 时也会丢弃其所有元素(Dropping a Vector Drops Its Elements)

像其他数据结构一样,当一个vector离开了它的作用域,它就会被释放,如Listing 8-10所示:

1
2
3
4
5
6
7
fn main() {
{
let v = vec![1, 2, 3, 4];

// do stuff with v
} // <- v goes out of scope and is freed here
}

Listing 8-10: Showing where the vector and its elements are droped

当vector被删除时,它的所有内容也会被删除,这意味着它所持有的整数将被清除。借用检查器确保仅在vector本身有效时才使用对向量内容的任何引用。

接下来,我们将学习下一个集合类型:String

常见集合

Rust标准库中包含一些非常有用的数据结构叫做结合(collections).大多数其他的数据类型代表一个特殊的值,但是集合可以包含多个值。不像内建的数组和元组类型,这些集合数据指向堆,意味着在编译时不需要知道数据量,并且可以随着程序运行而增加或减少。每一种集合有不同的性能和消耗,并且选择一个合适的集合,对你当前的情景是需要你慢慢学习的技巧。在本章,我们将会讨论三个集合,它们在Rust编程中很常用:

  • vector 允许你一个挨着一个地存储一系列数量的可变的值
  • string 是一个字符类型的集合。我们之前已经提到过String类型,但是在本章,我们将更加深入地讨论它。
  • hash map 允许你用一个特定的key关联一个值(value),它是成为映射的更通用数据结构的一种实现。

要学习标准库提供的其他类型的集合,请查看文档std::collections

我们将要讨论如何创建和更新vectors, strings,和hash map,以及他们每个类型的独特之处。

总结

Rust提供了将包分成多个crate,将crate分成模块,以及通过指定绝对或者相对路径从一个模块引用另一个模块中定义的项的方式。你可以指定绝对路径或者相对路径。这个路径可以通过use声明引入到作用域,所以你可以在多次使用时可以使用更短的路径。模块定义的代码默认是私有的,不过可以选择增加pub关键字使其定义为公有。

接下来,我们来看一些标准库提供的集合数据类型,你可以利用它们写出更漂亮整洁的代码。

全局操作符(The Glob Operator)

如果想将一个路径下所有公有项引入作用域,可以使用指定路径后跟*,glob操作符:

1
use std::collections::*;

这个use语句会将std::collections中定义的所有的公有项引入到当前作用域。使用全局操作符时一定要多加小心!全局操作符会使得我们难以推导作用域中有什么名称和它们是定义在何处。

全局操作符经常用于测试模块tests中,这时会将所有内容引入作用域;我们将在第十一章”How to Write Tests”部分中讲解。全局操作符有时也用于preclude模式;查看”标准库文档”(https://doc.rust-lang.org/std/prelude/index.html#other-preludes)了解更多细节。

0%