From 2aab841de074200202bad8c0bfd42190cb819546 Mon Sep 17 00:00:00 2001 From: Allan Downey Date: Sun, 30 Jan 2022 15:08:49 +0800 Subject: [PATCH 1/3] Update: unified format --- book/contents/basic/collections/hashmap.md | 40 +++++++++++----------- book/contents/basic/collections/intro.md | 4 +-- book/contents/basic/collections/vector.md | 14 ++++---- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/book/contents/basic/collections/hashmap.md b/book/contents/basic/collections/hashmap.md index e010581f..3f010b0a 100644 --- a/book/contents/basic/collections/hashmap.md +++ b/book/contents/basic/collections/hashmap.md @@ -1,8 +1,8 @@ # KV存储HashMap -和动态数组一样,`HashMap` 也是 Rust 标准库中提供的集合类型,但是又与动态数组不同,`HashMap` 中存储的是一一映射的 `KV `键值对,并提供了平均复杂度为 `O(1)` 的查询方法,当我们希望通过一个 `Key` 去查询值时,该类型非常有用,以致于 `Go语言` 将该类型设置成了语言级别的内置特性。 +和动态数组一样,`HashMap` 也是 Rust 标准库中提供的集合类型,但是又与动态数组不同,`HashMap` 中存储的是一一映射的 `KV `键值对,并提供了平均复杂度为 `O(1)` 的查询方法,当我们希望通过一个 `Key` 去查询值时,该类型非常有用,以致于 Go 语言将该类型设置成了语言级别的内置特性。 -Rust 中哈希类型(哈希映射)为 `HashMap`,在其它语言中,也有类似的数据结构,例如 `hash map`,`map`,`object`,`hash table`,`字典`等等,引用小品演员孙涛的一句台词:大家都是本地狐狸,别搁那装貂 :)。 +Rust 中哈希类型(哈希映射)为 `HashMap`,在其它语言中,也有类似的数据结构,例如 `hash map`,`map`,`object`,`hash table`,`字典` 等等,引用小品演员孙涛的一句台词:大家都是本地狐狸,别搁那装貂 :)。 ## 创建HashMap @@ -21,20 +21,20 @@ my_gems.insert("蓝宝石", 2); my_gems.insert("河边捡的误以为是宝石的破石头", 18); ``` -很简单对吧?跟其它语言没有区别,聪明的同学甚至能够猜到该 `HashMap` 的类型: `HashMap<&str,i32>`。 +很简单对吧?跟其它语言没有区别,聪明的同学甚至能够猜到该 `HashMap` 的类型:`HashMap<&str,i32>`。 -但是还有一点,你可能没有注意,那就是使用 `HashMap` 需要手动通过 `use ...` 从标准库中引入到我们当前的作用域中来,仔细回忆下,之前使用另外两个集合类型 `String` 和` Vec` 时,我们是否有手动引用过?答案是 `No`,因为 `HashMap` 并没有包含在Rust的[`prelude`](../../appendix/prelude.md)中(Rust为了简化用户使用,提前将最常用的类型自动引入到作用域中)。 +但是还有一点,你可能没有注意,那就是使用 `HashMap` 需要手动通过 `use ...` 从标准库中引入到我们当前的作用域中来,仔细回忆下,之前使用另外两个集合类型 `String` 和` Vec` 时,我们是否有手动引用过?答案是 `No`,因为 `HashMap` 并没有包含在 Rust 的 [`prelude`](../../appendix/prelude.md) 中(Rust为了简化用户使用,提前将最常用的类型自动引入到作用域中)。 -所有的集合类型都是动态的,意味着它们没有固定的内存大小,因此它们底层的数据都存储在内存堆上,然后通过一个存储在栈中的引用类型来访问。同时,跟其它集合类型一致,`HashMap` 也是内聚性的,即所有的`K`必须拥有同样的类型,`V`也是如此。 +所有的集合类型都是动态的,意味着它们没有固定的内存大小,因此它们底层的数据都存储在内存堆上,然后通过一个存储在栈中的引用类型来访问。同时,跟其它集合类型一致,`HashMap` 也是内聚性的,即所有的 `K` 必须拥有同样的类型,`V` 也是如此。 -> 跟Vec一样,如果预先知道要存储的KV对个数,可以使用 `HashMap::with_capacity(capacity)` 创建指定大小的HashMap,避免频繁的内存分配和拷贝,提升性能 +> 跟 `Vec` 一样,如果预先知道要存储的 `KV` 对个数,可以使用 `HashMap::with_capacity(capacity)` 创建指定大小的 `HashMap`,避免频繁的内存分配和拷贝,提升性能 #### 使用迭代器和collect方法创建 在实际使用中,不是所有的场景都能 `new` 一个哈希表后,然后悠哉悠哉的依次插入对应的键值对,而是可能会从另外一个数据结构中,获取到对应的数据,最终生成 `HashMap`。 -例如考虑一个场景,有一张表格中记录了足球联赛中各队伍名称和积分的信息,这张表如果被导入到Rust项目中,一个合理的数据结构是 `Vec<(String,u32)>` 类型,该数组中的元素是一个个元组,该数据结构跟表格数据非常契合:表格中的数据都是逐行存储,每一个行都存有一个 `(队伍名称,积分)` 的信息。 +例如考虑一个场景,有一张表格中记录了足球联赛中各队伍名称和积分的信息,这张表如果被导入到 Rust 项目中,一个合理的数据结构是 `Vec<(String, u32)>` 类型,该数组中的元素是一个个元组,该数据结构跟表格数据非常契合:表格中的数据都是逐行存储,每一个行都存有一个 `(队伍名称, 积分)` 的信息。 -但是在很多时候,又需要通过队伍名称来查询对应的积分,此时动态数组就不适用了,因此可以用 `HashMap` 来保存相关的**队伍名称 -> 积分**映射关系。 理想很骨感,现实很丰满,如何将 `Vec<(String, u32)>` 中的数据快速写入到`HashMap`中? +但是在很多时候,又需要通过队伍名称来查询对应的积分,此时动态数组就不适用了,因此可以用 `HashMap` 来保存相关的**队伍名称 -> 积分**映射关系。 理想很骨感,现实很丰满,如何将 `Vec<(String, u32)>` 中的数据快速写入到 `HashMap` 中? 一个动动脚趾头就能想到的笨方法如下: ```rust @@ -56,9 +56,9 @@ fn main() { } ``` -遍历列表,将每一个元组作为一对 `KV `插入到 `HashMap` 中,很简单,但是。。。也不太聪明的样子,换个词说就是 - 不够`rusty`。 +遍历列表,将每一个元组作为一对 `KV `插入到 `HashMap` 中,很简单,但是……也不太聪明的样子,换个词说就是 —— 不够 rusty。 -好在,Rust为我们提供了一个非常精妙的解决办法:先将 `Vec` 转为迭代器,接着通过 `collect` 方法,将迭代器中的元素收集后,转成 `HashMap`: +好在,Rust 为我们提供了一个非常精妙的解决办法:先将 `Vec` 转为迭代器,接着通过 `collect` 方法,将迭代器中的元素收集后,转成 `HashMap`: ```rust fn main() { use std::collections::HashMap; @@ -77,20 +77,20 @@ fn main() { 代码很简单,`into_iter` 方法将列表转为迭代器,接着通过 `collect` 进行收集,不过需要注意的是,`collect` 方法在内部实际上支持生成多种类型的目标集合,因为我们需要通过类型标注 `HashMap<_,_>` 来告诉编译器:请帮我们收集为 `HashMap` 集合类型,具体的 `KV` 类型,麻烦编译器您老人家帮我们推导。 -由此可见,Rust中的编译器时而小聪明,时而大聪明,不过好在,它大聪明的时候,会自家人知道自己事,总归会通知你一声: +由此可见,Rust 中的编译器时而小聪明,时而大聪明,不过好在,它大聪明的时候,会自家人知道自己事,总归会通知你一声: ```console -error[E0282]: type annotations needed 需要类型标注 +error[E0282]: type annotations needed // 需要类型标注 --> src/main.rs:10:9 | 10 | let teams_map = teams_list.into_iter().collect(); - | ^^^^^^^^^ consider giving `teams_map` a type 给予teams_map一个具体的类型 + | ^^^^^^^^^ consider giving `teams_map` a type // 给予 `teams_map` 一个具体的类型 ``` ## 所有权转移 `HashMap` 的所有权规则与其它 Rust 类型没有区别: - 若类型实现 `Copy` 特征,该类型会被复制进 `HashMap`,因此无所谓所有权 -- 若没实现 `Copy` 特征,所有权将被转移给`HashMap`中 +- 若没实现 `Copy` 特征,所有权将被转移给 `HashMap` 中 例如我参选帅气男孩时的场景再现: ```rust @@ -240,7 +240,7 @@ for word in text.split_whitespace() { println!("{:?}", map); ``` -上面代码中,新建一个 `map` 用于保存词语出现的次数,插入一个词语时会进行判断:若之前没有插入过,则使用该词语作Key,插入次数0作为Value,若之前插入过则取出之前统计的该词语出现的次数,对其加一。 +上面代码中,新建一个 `map` 用于保存词语出现的次数,插入一个词语时会进行判断:若之前没有插入过,则使用该词语作 `Key`,插入次数 0 作为 `Value`,若之前插入过则取出之前统计的该词语出现的次数,对其加一。 有两点值得注意: - `or_insert` 返回了 `&mut v` 引用,因此可以通过该可变引用直接修改 `map` 中对应的值 @@ -250,19 +250,19 @@ println!("{:?}", map); ## 哈希函数 你肯定比较好奇,为何叫哈希表,到底什么是哈希。 -先来设想下,如果要实现 `Key` 与 `Value` 的一一对应,是不是意味着我们要能比较两个 `Key` 的相等性?例如"a"和"b",1和2,当这些类型做Key且能比较时,可以很容易知道 `1` 对应的值不会错误的映射到 `2` 上,因为 `1` 不等于 `2`。因此,一个类型能否作为 `Key` 的关键就是是否能进行相等比较,或者说该类型是否实现了 `std::cmp::Eq` 特征。 +先来设想下,如果要实现 `Key` 与 `Value` 的一一对应,是不是意味着我们要能比较两个 `Key` 的相等性?例如 "a" 和 "b",1 和 2,当这些类型做 `Key` 且能比较时,可以很容易知道 `1` 对应的值不会错误的映射到 `2` 上,因为 `1` 不等于 `2`。因此,一个类型能否作为 `Key` 的关键就是是否能进行相等比较,或者说该类型是否实现了 `std::cmp::Eq` 特征。 -> f32和f64浮点数,没有实现 `std::cmp::Eq` 特征,因此不可以用作 `HashMap` 的 `Key` +> f32 和 f64 浮点数,没有实现 `std::cmp::Eq` 特征,因此不可以用作 `HashMap` 的 `Key` -好了,理解完这个,再来设想一点,若一个复杂点的类型作为Key,那怎么在底层对它进行存储,怎么使用它进行查询和比较? 是不是很棘手?好在我们有哈希函数:通过它把 `Key` 计算后映射为哈希值,然后使用该哈希值来进行存储、查询、比较等操作。 +好了,理解完这个,再来设想一点,若一个复杂点的类型作为 `Key`,那怎么在底层对它进行存储,怎么使用它进行查询和比较? 是不是很棘手?好在我们有哈希函数:通过它把 `Key` 计算后映射为哈希值,然后使用该哈希值来进行存储、查询、比较等操作。 但是问题又来了,如何保证不同 `Key` 通过哈希后的两个值不会相同?如果相同,那意味着我们使用不同的 `Key`,却查到了同一个结果,这种明显是错误的行为。 此时,就涉及到安全性跟性能的取舍了。 -若要追求安全,尽可能减少冲突,同时防止拒绝服务(Denial of Service, DoS)攻击,就要使用密码学安全的哈希函数,`HashMap` 就是使用了这样的哈希函数。反之若要追求性能,就需要使用没有那么安全的算法。 +若要追求安全,尽可能减少冲突,同时防止拒绝服务(Denial of Service, DoS)攻击,就要使用密码学安全的哈希函数,`HashMap` 就是使用了这样的哈希函数。反之若要追求性能,就需要使用没有那么安全的算法。 #### 高性能三方库 -因此若性能测试显示当前标准库默认的哈希函数不能满足你的性能需求,就需要去[`crates.io`](https://crates.io)上寻找其它的哈希函数实现,使用方法很简单: +因此若性能测试显示当前标准库默认的哈希函数不能满足你的性能需求,就需要去 [`crates.io`](https://crates.io) 上寻找其它的哈希函数实现,使用方法很简单: ```rust use std::hash::BuildHasherDefault; use std::collections::HashMap; diff --git a/book/contents/basic/collections/intro.md b/book/contents/basic/collections/intro.md index 4fc7a6b6..be8ae8e0 100644 --- a/book/contents/basic/collections/intro.md +++ b/book/contents/basic/collections/intro.md @@ -1,8 +1,8 @@ # 集合类型 -在Rust标准库中有这样一批原住民,它们天生贵族,当你看到的一瞬间,就能爱上它们,上面是我瞎编的,其实主要是离了它们不行,不信等会我介绍后,你放个狠话,非它们不用试试? +在 Rust 标准库中有这样一批原住民,它们天生贵族,当你看到的一瞬间,就能爱上它们,上面是我瞎编的,其实主要是离了它们不行,不信等会我介绍后,你放个狠话,非它们不用试试? -集合在Rust中是一类比较特殊的类型,因为Rust中大多数数据类型都只能代表一个特定的值,但是集合却可以代表一大堆值。而且与语言级别的数组、字符串类型不同,标准库里的这些家伙是分配在堆上,因此都可以进行动态的增加和减少。 +集合在 Rust 中是一类比较特殊的类型,因为 Rust 中大多数数据类型都只能代表一个特定的值,但是集合却可以代表一大堆值。而且与语言级别的数组、字符串类型不同,标准库里的这些家伙是分配在堆上,因此都可以进行动态的增加和减少。 瞧,第一个集合排着整体的队列登场了,它里面的每个元素都雄赳赳气昂昂跟在另外一个元素后面,大小、宽度、高度竟然全部一致,真是令人惊叹。 它就是 `Vector` 类型,允许你创建一个动态数组,它里面的元素是一个紧挨着另一个排列的。 diff --git a/book/contents/basic/collections/vector.md b/book/contents/basic/collections/vector.md index 80dad041..ecb4c0ea 100644 --- a/book/contents/basic/collections/vector.md +++ b/book/contents/basic/collections/vector.md @@ -7,7 +7,7 @@ 总之,当我们想拥有一个列表,里面都是相同类型的数据时,动态数组将会非常有用。 ## 创建动态数组 -在Rust中,有多种方式可以创建动态数组。 +在 Rust 中,有多种方式可以创建动态数组。 #### Vec::new 使用 `Vec::new` 创建动态数组是最 rusty 的方式,它调用了 `Vec` 中的 `new` 关联函数: @@ -31,7 +31,7 @@ v.push(1); let v = vec![1, 2, 3]; ``` -同样,此处的 `v` 也无需标注类型,编译器只需检查它内部的元素即可自动推导出 `v` 的类型是 `Vec` (Rust中,整数默认类型是i32,在[数值类型](../base-type/numbers.md#整数类型)中有详细介绍)。 +同样,此处的 `v` 也无需标注类型,编译器只需检查它内部的元素即可自动推导出 `v` 的类型是 `Vec` (Rust 中,整数默认类型是 `i32`,在[数值类型](../base-type/numbers.md#整数类型)中有详细介绍)。 ## 更新Vector 向数组尾部添加元素,可以使用 `push` 方法: @@ -98,7 +98,7 @@ v.push(6); println!("The first element is: {}", first); ``` -先不运行,来推断下结果,首先 `first = &v[0]` 进行了不可变借用,`v.push` 进行了可变借用,如果 `first` 在 `v.push` 之后不再使用,那么该段代码可以成功编译(原因见[引用的作用域](../ownership/borrowing.md#可变引用与不可变引用不能同时存在))。 +先不运行,来推断下结果,首先 `first = &v[0]` 进行了不可变借用,`v.push` 进行了可变借用,如果 `first` 在 `v.push` 之后不再使用,那么该段代码可以成功编译(原因见[引用的作用域](../ownership/borrowing.md#可变引用与不可变引用不能同时存在))。 可是上面的代码中,`first` 这个不可变借用在可变借用 `v.push` 后被使用了,那么妥妥的,编译器就会报错: ```console @@ -122,15 +122,15 @@ error: could not compile `collections` due to previous error 其实,按理来说,这两个引用不应该互相影响的:一个是查询元素,一个是在数组尾部插入元素,完全不相干的操作,为何编译器要这么严格呢? -原因在于:数组的大小是可变的,当旧数组的大小不够用时,Rust会重新分配一块更大的内存空间,然后把旧数组拷贝过来。这种情况下,之前的引用显然会指向一块无效的内存,这非常rusty - 对用户进行严格的教育。 +原因在于:数组的大小是可变的,当旧数组的大小不够用时,Rust 会重新分配一块更大的内存空间,然后把旧数组拷贝过来。这种情况下,之前的引用显然会指向一块无效的内存,这非常 rusty —— 对用户进行严格的教育。 其实想想,**在长大之后,我们感激人生路上遇到过的严师益友,正是因为他们,我们才在正确的道路上不断前行,虽然在那个时候,并不能理解他们**,而 Rust 就如那个良师益友,它不断的在纠正我们不好的编程习惯,直到某一天,你发现自己能写出一次性通过的漂亮代码时,就能明白它的良苦用心。 -> 若读者想要更深入的了解`Vec`,可以看看[Rustonomicon],其中从零手撸一个动态数组,非常适合深入学习 +> 若读者想要更深入的了解`Vec`,可以看看[Rustonomicon](https://nomicon.purewhite.io/vec/vec.html),其中从零手撸一个动态数组,非常适合深入学习 ## 迭代遍历Vector中的元素 -如果想要依次访问数组中的元素,可以使用迭代的方式去遍历数组,这种方式比用下标的方式去遍历数组更安全也更高效(每次下标访问都会触发数组边界检查): +如果想要依次访问数组中的元素,可以使用迭代的方式去遍历数组,这种方式比用下标的方式去遍历数组更安全也更高效(每次下标访问都会触发数组边界检查): ```rust let v = vec![1, 2, 3]; for i in &v { @@ -205,6 +205,6 @@ fn main() { 比枚举实现要稍微复杂一些,我们为 `V4` 和 `V6` 都实现了特征 `IpAddr`,然后将它俩的实例用 `Box::new` 包裹后,存在了数组 `v` 中,需要注意的是,这里必需手动的指定类型:`Vec>`,表示数组 `v` 存储的是特征 `IpAddr` 的对象,这样就实现了在数组中存储不同的类型。 -在实际使用场景中,特征对象数组要比枚举数组常见很多,主要原因在于[特征对象非常灵活](../trait/trait-object.md),而编译器对枚举的限制较多,且无法动态增加类型。 +在实际使用场景中,特征对象数组要比枚举数组常见很多,主要原因在于[特征对象](../trait/trait-object.md)非常灵活,而编译器对枚举的限制较多,且无法动态增加类型。 最后,如果你想要了解 `Vector `更多的用法,请参见本书的标准库解析章节:[`Vector`常用方法](../../std/vector.md) From af1f6144a6dc8e3255b75dee5b97639ef9ad130b Mon Sep 17 00:00:00 2001 From: Allan Downey Date: Sun, 30 Jan 2022 15:09:07 +0800 Subject: [PATCH 2/3] fix(result-error): fix link --- book/contents/basic/result-error/intro.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/book/contents/basic/result-error/intro.md b/book/contents/basic/result-error/intro.md index 5ed28f00..52f91a76 100644 --- a/book/contents/basic/result-error/intro.md +++ b/book/contents/basic/result-error/intro.md @@ -4,7 +4,7 @@ 社会演变至今,这种思想依然没变,甚至来到计算中的微观世界,也是如此。及时、准确的获知系统在发生什么,是程序设计的重中之重。因此能够准确的分辨函数返回值是正确的还是错误的、以及在发生错误时该怎么快速处理,成了程序设计语言的必备功能。 -Go 语言为人诟病的其中一点就是 `if err != nil {}` 的大量使用,缺乏一些程序设计的美感,不过我倒是觉得这种简单的方式也有其好处,就是阅读代码时的流畅感很强,你不需要过多的思考各种语法是什么意思。与 Go 语言不同,Rust 博采众家之长,实现了颇具自身色彩的返回值和错误处理体系,本章我们就高屋建瓴地来学习,更加深入的讲解见[此章](../../errors/intro.md). +Go 语言为人诟病的其中一点就是 ***if err != nil {}*** 的大量使用,缺乏一些程序设计的美感,不过我倒是觉得这种简单的方式也有其好处,就是阅读代码时的流畅感很强,你不需要过多的思考各种语法是什么意思。与 Go 语言不同,Rust 博采众家之长,实现了颇具自身色彩的返回值和错误处理体系,本章我们就高屋建瓴地来学习,更加深入的讲解见[此章](../../advance/errors/intro.md). ## Rust的错误哲学 错误对于软件来说是不可避免的,因此一门优秀的编程语言必须有其完整的错误处理哲学。在很多情况下,Rust 需要你承认自己的代码可能会出错,并提前采取行动,来处理这些错误。 @@ -13,4 +13,4 @@ Rust 中的错误主要分为两类: - **可恢复错误**,通常用于从系统全局角度来看可以接受的错误,例如处理用户的访问、操作等错误,这些错误只会影响某个用户自身的操作进程,而不会对系统的全局稳定性产生影响 - **不可恢复错误**,刚好相反,该错误通常是全局性或者系统性的错误,例如数组越界访问,系统启动时发生了影响启动流程的错误等等,这些错误的影响往往对于系统来说是致命的 -很多编程语言,并不会区分这些错误,而是直接采用异常的方式去处理。Rust没有异常,但是Rust也有自己的卧龙凤雏:`Result` 用于可恢复错误,`panic!` 用于不可恢复错误。 +很多编程语言,并不会区分这些错误,而是直接采用异常的方式去处理。Rust 没有异常,但是 Rust 也有自己的卧龙凤雏:`Result` 用于可恢复错误,`panic!` 用于不可恢复错误。 From ab22f5febc0c994909ed1eb47732d63dd289e3bb Mon Sep 17 00:00:00 2001 From: Allan Downey Date: Sun, 30 Jan 2022 15:28:38 +0800 Subject: [PATCH 3/3] Update: unified format --- book/contents/basic/result-error/panic.md | 26 +++++++++++----------- book/contents/basic/result-error/result.md | 26 +++++++++++----------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/book/contents/basic/result-error/panic.md b/book/contents/basic/result-error/panic.md index 7bd47e92..4dd6ca22 100644 --- a/book/contents/basic/result-error/panic.md +++ b/book/contents/basic/result-error/panic.md @@ -38,13 +38,13 @@ fn main() { v[99]; } ``` -上面的代码很简单,数组只有`3`个元素,我们却尝试去访问它的第`100`号元素(数组索引从`0`开始),那自然会崩溃。 +上面的代码很简单,数组只有 `3` 个元素,我们却尝试去访问它的第 `100` 号元素(数组索引从 `0` 开始),那自然会崩溃。 我们的读者里不乏正义之士,此时肯定要质疑,一个简单的数组越界访问,为何要直接让程序崩溃?是不是有些小题大作了? -如果有过C语言的经验,即使你越界了,问题不大,我依然尝试去访问,至于这个值是不是你想要的(`100`号内存地址也有可能有值,只不过是其它变量或者程序的!),抱歉,不归我管,我只负责取,你要负责管理好自己的索引访问范围。上面这种情况被称为**缓冲区溢出**,并可能会导致安全漏洞,例如攻击者可以通过索引来访问到数组后面不被允许的数据。 +如果有过C语言的经验,即使你越界了,问题不大,我依然尝试去访问,至于这个值是不是你想要的(`100` 号内存地址也有可能有值,只不过是其它变量或者程序的!),抱歉,不归我管,我只负责取,你要负责管理好自己的索引访问范围。上面这种情况被称为**缓冲区溢出**,并可能会导致安全漏洞,例如攻击者可以通过索引来访问到数组后面不被允许的数据。 -说实话,我宁愿程序崩溃,为什么?当你取到了一个不属于你的值,这在很多时候会导致程序上的逻辑bug! 有编程经验的人都知道这种逻辑上的bug是多么难被发现和修复!因此程序直接崩溃,然后告诉我们问题发生的位置,最后我们对此进行修复,这才是最合理的软件开发流程,而不是把问题藏着掖着: +说实话,我宁愿程序崩溃,为什么?当你取到了一个不属于你的值,这在很多时候会导致程序上的逻辑bug! 有编程经验的人都知道这种逻辑上的 BUG 是多么难被发现和修复!因此程序直接崩溃,然后告诉我们问题发生的位置,最后我们对此进行修复,这才是最合理的软件开发流程,而不是把问题藏着掖着: ```console 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 @@ -75,29 +75,29 @@ note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose bac 上面的代码就是一次栈展开(也称栈回溯),它包含了函数调用的顺序,当然按照逆序排列:最近调用的函数排在列表的最上方。因为咱们的 `main` 函数基本是最先调用的函数了,所以排在了倒数第二位,还有一个关注点,排在最顶部最后一个调用的函数是 `rust_begin_unwind`,该函数的目的就是进行栈展开,呈现这些列表信息给我们。 -要获取到栈回溯信息,你还需要开启 `debug` 标志,该标志在使用 `cargo run` 或者 `cargo build` 时自动开启(这两个操作默认是 `Debug` 运行方式). 同时,栈展开信息在不同操作系统或者 Rust 版本上也所有不同。 +要获取到栈回溯信息,你还需要开启 `debug` 标志,该标志在使用 `cargo run` 或者 `cargo build` 时自动开启(这两个操作默认是 `Debug` 运行方式)。同时,栈展开信息在不同操作系统或者 Rust 版本上也所有不同。 ## panic时的两种终止方式 当出现 `panic!` 时,程序提供了两种方式来处理终止流程: **栈展开** 和 **直接终止**。 其中,默认的方式就是 `栈展开`,这意味着 Rust 会回溯栈上数据和函数调用,因此也意味着更多的善后工作,好处是可以给出充分的报错信息和栈调用信息,便于事后的问题复盘。`直接终止`,顾名思义,不清理数据就直接退出程序,善后工作交与操作系统来负责。 -对于绝大多数用户,使用默认选择是最好的,但是当你关心最终编译出的二进制可执行文件大小时,那么可以尝试去使用直接终止的方式,例如下面的配置修改 `Cargo.toml` 文件,实现在[`release`](../first-try/cargo.md#手动编译和运行项目)模式下遇到 `panic` 直接终止: +对于绝大多数用户,使用默认选择是最好的,但是当你关心最终编译出的二进制可执行文件大小时,那么可以尝试去使用直接终止的方式,例如下面的配置修改 `Cargo.toml` 文件,实现在 [`release`](../first-try/cargo.md#手动编译和运行项目) 模式下遇到 `panic` 直接终止: ```rust [profile.release] panic = 'abort' ``` -## 线程`panic`后,程序是否会终止? +## 线程 `panic` 后,程序是否会终止? 长话短说,如果是 `main` 线程,则程序会终止,如果是其它子线程,该线程会终止,但是不会影响 `main` 线程。因此,尽量不要在 `main` 线程中做太多任务,将这些任务交由子线程去做,就算子线程 `panic` 也不会导致整个程序的结束。 -具体解析见[panic原理剖析](#panic原理剖析) +具体解析见 [panic原理剖析](#panic原理剖析)。 ## 何时该使用panic! 下面让我们大概罗列下何时适合使用 `panic`,也许经过之前的学习,你已经能够对 `panic` 的使用有了自己的看法,但是我们还是会罗列一些常见的用法来加深你的理解。 -先来一点背景知识,在前面章节我们粗略讲过 `Result` 这个枚举类型,它是用来表示函数的返回结果: +先来一点背景知识,在前面章节我们粗略讲过 `Result` 这个枚举类型,它是用来表示函数的返回结果: ```rust enum Result { Ok(T), @@ -110,17 +110,17 @@ use std::net::IpAddr; let home: IpAddr = "127.0.0.1".parse().unwrap(); ``` -上面的 `parse` 方法试图将字符串 `"127.0.0.1" `解析为一个IP地址类型 `IpAddr`,它返回一个 `Result` 类型,如果解析成功,则把 `Ok(IpAddr)` 中的值赋给 `home`,如果失败,则不处理 `Err(E)`,而是直接 `panic`。 +上面的 `parse` 方法试图将字符串 `"127.0.0.1" `解析为一个IP地址类型 `IpAddr`,它返回一个 `Result` 类型,如果解析成功,则把 `Ok(IpAddr)` 中的值赋给 `home`,如果失败,则不处理 `Err(E)`,而是直接 `panic`。 因此 `unwrap` 简而言之:成功则返回值,失败则 `panic`,总之不进行任何错误处理。 #### 示例、原型、测试 -这几个场景下,需要快速地搭建代码,错误处理会拖慢编码的速度,也不是特别有必要,因此通过`unwrap`、`expect`等方法来处理是最快的。 +这几个场景下,需要快速地搭建代码,错误处理会拖慢编码的速度,也不是特别有必要,因此通过 `unwrap`、`expect` 等方法来处理是最快的。 同时,当我们回头准备做错误处理时,可以全局搜索这些方法,不遗漏地进行替换。 #### 你确切的知道你的程序是正确时,可以使用panic -因为 `panic` 的触发方式比错误处理要简单,因此可以让代码更清晰,可读性也更加好,当我们的代码注定是正确时,你可以用 `unrawp` 等方法直接进行处理,反正也不可能`panic`: +因为 `panic` 的触发方式比错误处理要简单,因此可以让代码更清晰,可读性也更加好,当我们的代码注定是正确时,你可以用 `unrawp` 等方法直接进行处理,反正也不可能 `panic` : ```rust use std::net::IpAddr; let home: IpAddr = "127.0.0.1".parse().unwrap(); @@ -149,9 +149,9 @@ let home: IpAddr = "127.0.0.1".parse().unwrap(); 当调用 `panic!` 宏时,它会 1. 格式化 `panic` 信息,然后使用该信息作为参数,调用 `std::panic::panic_any()` 函数 -2. `panic_any` 会检查应用是否使用了 `panic hook`,如果使用了,该 `hook` 函数就会被调用(hook是一个钩子函数,是外部代码设置的,用于在panic触发时,执行外部代码所需的功能) +2. `panic_any` 会检查应用是否使用了 `panic hook`,如果使用了,该 `hook` 函数就会被调用(`hook` 是一个钩子函数,是外部代码设置的,用于在 `panic` 触发时,执行外部代码所需的功能) 3. 当 `hook` 函数返回后,当前的线程就开始进行栈展开:从 `panic_any` 开始,如果寄存器或者栈因为某些原因信息错乱了,那很可能该展开会发生异常,最终线程会直接停止,展开也无法继续进行 -4. 展开的过程是一帧一帧的去回溯整个栈,每个帧的数据都会随之被丢弃,但是在展开过程中,你可能会遇到被用户标记为 `catching` 的帧(通过 `std::panic::catch_unwind()` 函数标记),此时用户提供的 `catch` 函数会被调用,展开也随之停止:当然,如果 `catch` 选择在内部调用 `std::panic::resume_unwind()` 函数,则展开还会继续。 +4. 展开的过程是一帧一帧的去回溯整个栈,每个帧的数据都会随之被丢弃,但是在展开过程中,你可能会遇到被用户标记为 `catching` 的帧(通过 `std::panic::catch_unwind()` 函数标记),此时用户提供的 `catch` 函数会被调用,展开也随之停止:当然,如果 `catch` 选择在内部调用 `std::panic::resume_unwind()` 函数,则展开还会继续。 还有一种情况,在展开过程中,如果展开本身 `panic` 了,那展开线程会终止,展开也随之停止。 diff --git a/book/contents/basic/result-error/result.md b/book/contents/basic/result-error/result.md index 8e573f3a..db23cb25 100644 --- a/book/contents/basic/result-error/result.md +++ b/book/contents/basic/result-error/result.md @@ -1,9 +1,9 @@ # 可恢复的错误Result 还记得上一节中,提到的关于文件读取的思考题吧?当时我们解决了读取文件时遇到不可恢复错误该怎么处理的问题,现在来看看,读取过程中,正常返回和遇到可以恢复的错误时该如何处理。 -假设,我们有一台消息服务器,每个用户都通过 websocket 连接到该服务器来接收和发送消息,该过程就涉及到 socket 文件的读写,那么此时,如果一个用户的读写发生了错误,显然不能直接panic,否则服务器会直接崩溃,所有用户都会断开连接,因此我们需要一种更温和的错误处理方式:`Result`。 +假设,我们有一台消息服务器,每个用户都通过 websocket 连接到该服务器来接收和发送消息,该过程就涉及到 socket 文件的读写,那么此时,如果一个用户的读写发生了错误,显然不能直接 `panic`,否则服务器会直接崩溃,所有用户都会断开连接,因此我们需要一种更温和的错误处理方式:`Result`。 -之前章节有提到过,`Result` 是一个枚举类型,定义如下: +之前章节有提到过,`Result` 是一个枚举类型,定义如下: ```rust enum Result { Ok(T), @@ -24,7 +24,7 @@ fn main() { > > 有几种常用的方式,此处更推荐第二种方法: > - 第一种是查询标准库或者三方库文档,搜索 `File`,然后找到它的 `open` 方法 -> - 在[Rust IDE](../../first-try/editor.md)章节,我们推荐了 `VSCode` IED和 `rust-analyzer` 插件,如果你成功安装的话,那么就可以在 `VScode` 中很方便的通过代码跳转的方式查看代码,同时 `rust-analyzer` 插件还会对代码中的类型进行标注,非常方便好用! +> - 在 [Rust IDE](../../first-try/editor.md) 章节,我们推荐了 `VSCode` IDE 和 `rust-analyzer` 插件,如果你成功安装的话,那么就可以在 `VSCode` 中很方便的通过代码跳转的方式查看代码,同时 `rust-analyzer` 插件还会对代码中的类型进行标注,非常方便好用! > - 你还可以尝试故意标记一个错误的类型,然后让编译器告诉你: ```rust let f: u32 = File::open("hello.txt"); @@ -44,7 +44,7 @@ error[E0308]: mismatched types 上面代码,故意将 `f` 类型标记成整形,编译器立刻不乐意了,你是在忽悠我吗?打开文件操作返回一个整形?来,大哥来告诉你返回什么:`std::result::Result`,我的天呐,怎么这么长的类型! -别慌,其实很简单,首先 `Result` 本身是定义在 `std::result` 中的,但是因为 `Result` 很常用,所以就被包含在了[`prelude`](../../appendix/prelude.md)中(将常用的东东提前引入到当前作用域内),因此无需手动引入 `std::result::Result`,那么返回类型可以简化为 `Result`,你看看是不是很像标准的 `Result` 枚举定义?只不过 `T` 被替换成了具体的类型 `std::fs::File`,是一个文件句柄类型,`E` 被替换成 `std::io::Error`,是一个 IO 错误类型. +别慌,其实很简单,首先 `Result` 本身是定义在 `std::result` 中的,但是因为 `Result` 很常用,所以就被包含在了 [`prelude`](../../appendix/prelude.md) 中(将常用的东东提前引入到当前作用域内),因此无需手动引入 `std::result::Result`,那么返回类型可以简化为 `Result`,你看看是不是很像标准的 `Result` 枚举定义?只不过 `T` 被替换成了具体的类型 `std::fs::File`,是一个文件句柄类型,`E` 被替换成 `std::io::Error`,是一个 IO 错误类型. 这个返回值类型说明 `File::open` 调用如果成功则返回一个可以进行读写的文件句柄,如果失败,则返回一个 IO 错误:文件不存在或者没有访问文件的权限等。总之 `File::open` 需要一个方式告知调用者是成功还是失败,并同时返回具体的文件句柄(成功)或错误信息(失败),万幸的是,这些信息可以通过 `Result` 枚举提供: ```rust @@ -62,7 +62,7 @@ fn main() { } ``` -代码很清晰,对打开文件后的 `Result` 类型进行匹配取值,如果是成功,则将 `Ok(file)` 中存放的的文件句柄 `file` 赋值给 `f`,如果失败,则将 `Err(error)` 中存放的错误信息 `error` 使用 `panic` 抛出来,进而结束程序,这非常符合上文提到过的 `panic` 使用场景。 +代码很清晰,对打开文件后的 `Result` 类型进行匹配取值,如果是成功,则将 `Ok(file)` 中存放的的文件句柄 `file` 赋值给 `f`,如果失败,则将 `Err(error)` 中存放的错误信息 `error` 使用 `panic` 抛出来,进而结束程序,这非常符合上文提到过的 `panic` 使用场景。 好吧,也没有那么合理 :) @@ -93,12 +93,12 @@ fn main() { - 如果是文件不存在错误 `ErrorKind::NotFound`,就创建文件,这里创建文件`File::create` 也是返回 `Result`,因此继续用 `match` 对其结果进行处理:创建成功,将新的文件句柄赋值给 `f`,如果失败,则 `panic` - 剩下的错误,一律 `panic` -虽然很清晰,但是代码还是有些啰嗦,我们会在[简化错误处理](../../errors/simplify.md)一章重点讲述如何写出更优雅的错误。 +虽然很清晰,但是代码还是有些啰嗦,我们会在[简化错误处理](../../advance/errors/simplify.md)一章重点讲述如何写出更优雅的错误。 ## 失败就 panic: unwrap 和 expect 上一节中,已经看到过这两兄弟的简单介绍,这里再来回顾下。 -在不需要处理错误的场景,例如写原型、示例时,我们不想使用 `match` 去匹配 `Result `以获取其中的 `T` 值,因为 `match` 的穷尽匹配特性,你总要去处理下 `Err` 分支。那么有没有办法简化这个过程?有,答案就是 `unwrap` 和 `expect`。 +在不需要处理错误的场景,例如写原型、示例时,我们不想使用 `match` 去匹配 `Result ` 以获取其中的 `T` 值,因为 `match` 的穷尽匹配特性,你总要去处理下 `Err` 分支。那么有没有办法简化这个过程?有,答案就是 `unwrap` 和 `expect`。 它们的作用就是,如果返回成功,就将 `Ok(T)` 中的值取出来,如果失败,就直接 `panic`,真的勇士绝不多BB,直接崩溃。 @@ -166,7 +166,7 @@ fn read_username_from_file() -> Result { 有几点值得注意: - 该函数返回一个 `Result` 类型,当读取用户名成功时,返回 `Ok(String)`,失败时,返回 `Err(io:Error)` -- `File::open` 和 `f.read_to_string` 返回的 `Result` 中的 `E` 就是 `io::Error` +- `File::open` 和 `f.read_to_string` 返回的 `Result` 中的 `E` 就是 `io::Error` 由此可见,该函数将 `io::Error` 的错误往上进行传播,该函数的调用者最终会对 `Result` 进行再处理,至于怎么处理就是调用者的事,如果是错误,它可以选择继续向上传播错误,也可以直接 `panic`,亦或将具体的错误原因包装后写入 socket 中呈现给终端用户。 @@ -202,7 +202,7 @@ let mut f = match f { 虽然 `?` 和 `match` 功能一致,但是事实上 `?` 会更胜一筹。何解? -想象一下,一个设计良好的系统中,肯定有自定义的错误特征,错误之间很可能会存在上下级关系,例如标准库中的 `std::io::Error `和 `std::error::Error`,前者是io相关的错误结构体,后者是一个最最通用的标准错误特征,同时前者实现了后者,因此 `std::io::Error` 可以转换为 `std:error::Error`。 +想象一下,一个设计良好的系统中,肯定有自定义的错误特征,错误之间很可能会存在上下级关系,例如标准库中的 `std::io::Error `和 `std::error::Error`,前者是 IO 相关的错误结构体,后者是一个最最通用的标准错误特征,同时前者实现了后者,因此 `std::io::Error` 可以转换为 `std:error::Error`。 明白了以上的错误转换,`?` 的更胜一筹就很好理解了,它可以自动进行类型提升(转换): ```rust @@ -231,7 +231,7 @@ fn read_username_from_file() -> Result { Ok(s) } ``` -瞧见没? `?` 还能实现链式调用,`File::open` 遇到错误就返回,没有错误就将 `Ok` 中的值取出来用于下一个方法调用,简直太精妙了,从 Go 语言过来的我,内心狂喜(其实学 Rust 的苦和痛我才不会告诉你们)。 +瞧见没? `?` 还能实现链式调用,`File::open` 遇到错误就返回,没有错误就将 `Ok` 中的值取出来用于下一个方法调用,简直太精妙了,从 Go 语言过来的我,内心狂喜(其实学 Rust 的苦和痛我才不会告诉你们)。 不仅有更强,还要有最强,我不信还有人比我更短(不要误解): ```rust @@ -270,7 +270,7 @@ fn first(arr: &[i32]) -> Option<&i32> { arr.get(0) } ``` -有一句话怎么说?没有需求,制造需求也要上。。。大家别跟我学习,这是软件开发大忌。只能用代码洗洗眼了: +有一句话怎么说?没有需求,制造需求也要上……大家别跟我学习,这是软件开发大忌。只能用代码洗洗眼了: ```rust fn last_char_of_first_line(text: &str) -> Option { text.lines().next()?.chars().last() @@ -298,7 +298,7 @@ fn main() { let f = File::open("hello.txt")?; } ``` -因为 `?` 要求 `Result` 形式的返回值,而 `main` 函数的返回是 `()`,因此无法满足,那是不是就无解了呢? +因为 `?` 要求 `Result` 形式的返回值,而 `main` 函数的返回是 `()`,因此无法满足,那是不是就无解了呢? 实际上 Rust 还支持另外一种形式的 `main` 函数: ```rust @@ -314,7 +314,7 @@ fn main() -> Result<(), Box> { 这样就能使用 `?` 提前返回了,同时我们又一次看到了`Box` 特征对象,因为 `std::error:Error` 是 Rust 中抽象层次最高的错误,其它标准库中的错误都实现了该特征,因此我们可以用该特征对象代表一切错误,就算 `main` 函数中调用任何标准库函数发生错误,都可以通过 `Box` 这个特征对象进行返回. -至于 `main` 函数可以有多种返回值,那是因为实现了[std::process::Termination](https://doc.rust-lang.org/std/process/trait.Termination.html)特征,目前为止该特征还没进入稳定版Rust中,也许未来你可以为自己的类型实现该特征! +至于 `main` 函数可以有多种返回值,那是因为实现了 [std::process::Termination](https://doc.rust-lang.org/std/process/trait.Termination.html) 特征,目前为止该特征还没进入稳定版 Rust 中,也许未来你可以为自己的类型实现该特征! 至此,Rust 的基础内容学习已经全部完成,下面我们将学习 Rust 的高级进阶内容,正式开启你的高手之路。