diff --git a/book/contents/about-book.md b/book/contents/about-book.md index da77fb8b..ab0f75b0 100644 --- a/book/contents/about-book.md +++ b/book/contents/about-book.md @@ -1,34 +1,38 @@ -# Rust语言圣经(Rust course) +# Rust 语言圣经 (Rust course) - 官方网址: https://course.rs - 修订时间: **尚未发行** -- Rust版本: Rust edition 2021 -- QQ交流群: 1009730433 +- Rust 版本: Rust edition 2021 +- QQ 交流群:1009730433 ### 教程简介 -`Rust语言圣经`涵盖从**入门到精通**所需的全部Rust知识,目录及内容都经过深思熟虑的设计,同时语言生动幽默,行文流畅自如,摆脱技术书籍常有的机器味和晦涩感。 +`Rust 语言圣经`涵盖从**入门到精通**所需的全部 Rust 知识,目录及内容都经过深思熟虑的设计,同时语言生动幽默,行文流畅自如,摆脱技术书籍常有的机器味和晦涩感。 -在Rust基础教学的同时,我们还提供了: -- **深入度**, 在基础教学的同时, 提供了深入剖析, 浅尝辄止并不能让我们站上紫禁之巅 -- **性能优化**,选择Rust, 意味着就要追求性能, 因此你需要体系化的了解性能优化 -- **专题**,将Rust高级内容通过专题的方式一一呈现,内容内聚性极强 +在 Rust 基础教学的同时,我们还提供了: +- **深入度**,在基础教学的同时,提供了深入剖析,浅尝辄止并不能让我们站上紫禁之巅 +- **性能优化**,选择 Rust,意味着就要追求性能,因此你需要体系化的了解性能优化 +- **专题**,将 Rust 高级内容通过专题的方式一一呈现,内容内聚性极强 - **难点索引**,作为一本工具书,优秀的索引能力非常重要,遗忘不可怕,找不到才可怕 -- **场景化模版**,程序员上网查询如何操作文件是常事,没有人能记住所有代码,场景化模版可解君忧 -- **开源库推荐**, 根据场景推荐高质量的开源库,降低Rust上手门槛 +- **场景化模版**,程序员上网查询如何操作是常事,没有人能记住所有代码,场景化模版可解君忧 +- **开源库推荐**, 根据场景推荐高质量的开源库,降低 Rust 上手门槛 -总之在写作过程中我们始终铭记初心:为中国用户打造一本**全面的、深入的、持续更新的**Rust教程。 新手用来入门,老手用来提高,高手用来提升生产力。 +总之在写作过程中我们始终铭记初心:为中国用户打造一本**全面的、深入的、持续更新的** Rust 教程。 新手用来入门,老手用来提高,高手用来提升生产力。 ### 开源说明 -Rust语言圣经是**完全开源**的电子书, 每个章节都至少用时4-6个小时才能初步完稿,牺牲了大量休闲娱乐、陪伴家人的时间,还没有任何钱赚,**如果大家觉得这本书作者真的用心了,希望你能帮我们点一个`star`**,感激不尽:) -在开源版权上,我们选择了[No License](https://www.google.com.hk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&ved=2ahUKEwigkv-KtMT0AhXFdXAKHdI4BCcQFnoECAQQAw&url=https%3A%2F%2Fchoosealicense.com%2Fno-permission%2F&usg=AOvVaw3M2Q4IbdhnpJ2K71TF7SPB),这意味着读者可以随意的fork和阅读,但是**不能私下修改后再包装分发**,如果有这方面的需求,请联系我们,我们不会收钱,只是希望知道谁通过什么方式分发了这本书的部分内容,望理解. +Rust 语言圣经是**完全开源**的电子书,每个章节都至少用时 4-6 个小时才能初步完稿,牺牲了大量休闲娱乐,陪伴家人的时间。而且还没有任何钱赚,**如果大家觉得这本书的作者真的用心了,希望你能帮我们点一个 🌟[star](https://github.com/sunface/rust-course)**,感激不尽!:) +在开源版权上,我们选择了 [No License](https://www.google.com.hk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&ved=2ahUKEwigkv-KtMT0AhXFdXAKHdI4BCcQFnoECAQQAw&url=https%3A%2F%2Fchoosealicense.com%2Fno-permission%2F&usg=AOvVaw3M2Q4IbdhnpJ2K71TF7SPB),这意味着**读者可以随意的 fork 和阅读,但是不能私下修改后再包装分发。** + +如果有这方面的需求,请联系我们。我们不会收钱,只是希望知道谁通过什么方式分发了这本书的部分内容,望理解。 + +### Rust 社区 + +与国外的 Rust 发展如火如荼相比,国内的近况不是特别理想。导致目前这种状况的原因,我个人认为有以下几点: -### Rust社区 -与国外的Rust发展如火如荼相比,国内的近况不是特别理想。导致目前这种状况的原因,我个人认为有以下几点原因: 1. 上手难度大,学习曲线陡峭 -2. 英文资料难学(阅读较难的技术内容,需要精准阅读,因此对外语能力要求较高),中文资料也不太好学(内容全面度、实时性,晦涩难懂等) +2. 英文资料难学(阅读较难的技术内容,需要精准阅读,因此对外语能力要求较高),中文资料也不太好学(内容全面度、实时性,晦涩难懂等) 3. 没有体系化的学习路线,新人往往扫完一遍入门书籍,就不知道何去何从 -为此,我整了一本书和一个社区,欢迎大家的加入: -- QQ群:1009730433 +为此,我整了一本书和一个社区,欢迎大家的加入: +- QQ 群:1009730433 diff --git a/book/contents/advance/smart-pointer/cell-refcell.md b/book/contents/advance/smart-pointer/cell-refcell.md index 9de55da5..c611fbc6 100644 --- a/book/contents/advance/smart-pointer/cell-refcell.md +++ b/book/contents/advance/smart-pointer/cell-refcell.md @@ -345,7 +345,7 @@ fn retain_even(nums: &mut Vec) { ## 总结 -`Cell`和`RefCell`都为我们带来了内部可见性这个重要特性,同时还将借用规则的检查从编译期推迟到运行期,但是这个检查并不能被绕过,该来早晚还是会来,`RefCell在运行期的报错会造成`panic` +`Cell`和`RefCell`都为我们带来了内部可变性这个重要特性,同时还将借用规则的检查从编译期推迟到运行期,但是这个检查并不能被绕过,该来早晚还是会来,`RefCell`在运行期的报错会造成`panic` `RefCell`适用于编译器误报或者一个引用被在多个代码中使用、修改以至于难于管理借用关系时,还有就是需要内部可变性时。 diff --git a/book/contents/basic/base-type/numbers.md b/book/contents/basic/base-type/numbers.md index 44262d94..88f70835 100644 --- a/book/contents/basic/base-type/numbers.md +++ b/book/contents/basic/base-type/numbers.md @@ -23,7 +23,7 @@ Rust使用一个相对传统的语法来创建整数(`1`,`2`,...)和浮点数(`1 | 128-位 | `i128` | `u128` | | 视架构而定 | `isize` | `usize` | -类型定义的形式统一为:有无符号 + 类型大小(位数)。**无符号数**表示数字只能取正数,而**有符号**则表示数字即可以取正数又可以取负数。就像在纸上写数字一样:当要强调符号时,数字前面可以带上正号或负号;然而,当很明显确定数字为正数时,就不需要加上正号了。有符号数字以[二进制补码](https://en.wikipedia.org/wiki/Two%27s_complement)形式存储。 +类型定义的形式统一为:有无符号 + 类型大小(位数)。**无符号数**表示数字只能取正数,而**有符号**则表示数字即可以取正数又可以取负数。就像在纸上写数字一样:当要强调符号时,数字前面可以带上正号或负号;然而,当很明显确定数字为正数时,就不需要加上正号了。有符号数字以[补码](https://en.wikipedia.org/wiki/Two%27s_complement)形式存储。 每个有符号类型规定的数字范围是 -(2n - 1) ~ 2n - 1 - 1,其中 `n` 是该定义形式的位长度。因此 `i8` 可存储数字范围是 -(27) ~ 27 - 1,即 -128 ~ 127。无符号类型可以存储的数字范围是 0 ~ 2n - 1,所以 `u8` 能够存储的数字为 0 ~ 28 - 1,即 0 ~ 255。 @@ -48,11 +48,11 @@ Rust使用一个相对传统的语法来创建整数(`1`,`2`,...)和浮点数(`1 > > 比方说有一个 `u8` ,它可以存放从 0 到 255 的值。那么当你将其修改为范围之外的值,比如 256,则会发生**整型溢出**。关于这一行为 Rust 有一些有趣的规则:当在 debug 模式编译时,Rust 会检查整型溢出,若存在这些问题,则使程序在编译时 *panic*(崩溃,Rust 使用这个术语来表明程序因错误而退出)。 > -> 在当使用 `--release` 参数进行 release 模式构建时,Rust **不**检测溢出。相反当检测到整型溢出时,Rust 会进行一种被称为二进制补码的方式进行(*two’s complement wrapping*)操作。简而言之,大于该类型最大值的数值会被补码转换成该类型能够支持的对应数字的最小值。比如在 `u8` 的情况下,256 变成 0,257 变成 1,依此类推。程序不会 panic,但是该变量的值可能不是你期望的值。依赖整型溢出的行为不是一种正确的做法。 +> 在当使用 `--release` 参数进行 release 模式构建时,Rust **不**检测溢出。相反,当检测到整型溢出时,Rust 会按照补码循环溢出(*two’s complement wrapping*)的规则处理。简而言之,大于该类型最大值的数值会被补码转换成该类型能够支持的对应数字的最小值。比如在 `u8` 的情况下,256 变成 0,257 变成 1,依此类推。程序不会 panic,但是该变量的值可能不是你期望的值。依赖这种默认行为的代码都应该被认为是错误的代码。 > -> 要显式处理溢出的可能性,可以使用标准库针对原始数字类型提供的以下的一系列方法: +> 要显式处理可能的溢出,可以使用标准库针对原始数字类型提供的这些方法: > -> - 使用 `wrapping_*` 方法在所有模式下进行包裹,例如 `wrapping_add` +> - 使用 `wrapping_*` 方法在所有模式下都按照补码循环溢出规则处理,例如 `wrapping_add` > - 如果使用 `checked_*` 方法时发生溢出,则返回 `None` 值 > - 使用 `overflowing_*` 方法返回该值和一个指示是否存在溢出的布尔值 > - 使用 `saturating_*` 方法使值达到最小值或最大值 diff --git a/book/contents/basic/collections/hashmap.md b/book/contents/basic/collections/hashmap.md index 77d08665..df4bf182 100644 --- a/book/contents/basic/collections/hashmap.md +++ b/book/contents/basic/collections/hashmap.md @@ -1,12 +1,12 @@ # 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 -跟创建动态数组`Vec`的方法类似, 可以使用`new`方法来创建`HashMap`,然后通过`insert`方法插入键值对. +跟创建动态数组 `Vec` 的方法类似,可以使用 `new` 方法来创建` HashMap`,然后通过` insert` 方法插入键值对。 #### 使用new方法创建 ```rust @@ -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`. +在实际使用中,不是所有的场景都能 `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; @@ -75,9 +75,9 @@ fn main() { } ``` -代码很简单,`into_iter`方法将列表转为迭代器,接着通过`collect`进行收集,不过需要注意的是,`collect`方法在内部实际上支持生成多种类型的目标集合,因为我们需要通过类型标注`HashMap<_,_>`来告诉编译器:请帮我们收集为`HashMap`集合类型,具体的`KV`类型,麻烦编译器你老人家帮我们推导。 +代码很简单,`into_iter` 方法将列表转为迭代器,接着通过 `collect` 进行收集,不过需要注意的是,`collect` 方法在内部实际上支持生成多种类型的目标集合,因为我们需要通过类型标注 `HashMap<_,_>` 来告诉编译器:请帮我们收集为 `HashMap` 集合类型,具体的 `KV` 类型,麻烦编译器您老人家帮我们推导。 -由此可见,Rust中的编译器时而小聪明,时而大聪明,不过好在,它大聪明的时候,会自家人知道自己事,总归会通知你一声: +由此可见,Rust中的编译器时而小聪明,时而大聪明,不过好在,它大聪明的时候,会自家人知道自己事,总归会通知你一声: ```console error[E0282]: type annotations needed 需要类型标注 --> src/main.rs:10:9 @@ -88,9 +88,9 @@ error[E0282]: type annotations needed 需要类型标注 ## 所有权转移 -`HashMap`的所有权规则与其它Rust类型没有区别: -- 若类型实现`Copy`特征,该类型会被复制进`HashMap`, 因此无所谓所有权 -- 若没实现`Copy`特征,所有权将被转移给`HashMap`中 +`HashMap` 的所有权规则与其它 Rust 类型没有区别: +- 若类型实现 `Copy` 特征,该类型会被复制进 `HashMap`,因此无所谓所有权 +- 若没实现 `Copy` 特征,所有权将被转移给`HashMap`中 例如我参选帅气男孩时的场景再现: ```rust @@ -123,10 +123,10 @@ error[E0382]: borrow of moved value: `name` | ^^^^ value borrowed here after move ``` -提示很清晰,`name`是`String`类型,因此它受到所有权的限制,在`insert`时,它的所有权被转移给`handsome_boys`,最后在使用时,无情但是意料之中的报错。 +提示很清晰,`name` 是 `String` 类型,因此它受到所有权的限制,在 `insert` 时,它的所有权被转移给 `handsome_boys`,所以最后在使用时,会遇到这个无情但是意料之中的报错。 -**如果你使用引用类型放入HashMap中**, 请确保该类型至少跟`HashMap`获得一样久: +**如果你使用引用类型放入HashMap中**,请确保该引用的生命周期至少跟 `HashMap` 活得一样久: ```rust fn main() { use std::collections::HashMap; @@ -143,7 +143,7 @@ fn main() { } ``` -上面代码,我们借用`name`获取了它的引用,然后插入到`handsome_boys`中,至此一切都很完美。但是紧接着,就通过`drop`函数手动将`name`字符串从内存中移除,再然后就报错了: +上面代码,我们借用 `name` 获取了它的引用,然后插入到 `handsome_boys` 中,至此一切都很完美。但是紧接着,就通过 `drop` 函数手动将 `name` 字符串从内存中移除,再然后就报错了: ```console handsome_boys.insert(&name, age); | ----- borrow of `name` occurs here // name借用发生在此处 @@ -154,10 +154,10 @@ fn main() { | ------------- borrow later used here // 所有权转移后,还试图使用name ``` -最终,某人因为过于无耻,真正的被除名了:) +最终,某人因为过于无耻,真正的被除名了 :) ## 查询HashMap -通过`get`方法可以获取元素: +通过 `get` 方法可以获取元素: ```rust use std::collections::HashMap; @@ -171,10 +171,10 @@ let score: Option<&i32> = scores.get(&team_name); ``` 上面有几点需要注意: -- `get`方法返回一个`Option<&i32>`类型:当查询不到时,会返回一个`None`,查询到时返回`Some(&i32)` -- `&i32`是对`HashMap`中值的借用,如果不使用借用,可能会发生所有权的转移 +- `get` 方法返回一个 `Option<&i32> `类型:当查询不到时,会返回一个 `None`,查询到时返回 `Some(&i32)` +- `&i32` 是对 `HashMap` 中值的借用,如果不使用借用,可能会发生所有权的转移 -还可以通过循环的方式依次遍历`KV`对: +还可以通过循环的方式依次遍历 `KV` 对: ```rust use std::collections::HashMap; @@ -187,7 +187,7 @@ for (key, value) in &scores { println!("{}: {}", key, value); } ``` -最终输出: +最终输出: ```console Yellow: 50 Blue: 10 @@ -224,7 +224,7 @@ fn main() { 具体的解释在代码注释中已有,这里不再进行赘述。 #### 在已有值的基础上更新 -另一个常用场景如下:查询某个`key`对应的值,若不存在则插入新值,若存在则对已有的值进行更新,例如在文本中统计词语出现的次数: +另一个常用场景如下:查询某个 `key` 对应的值,若不存在则插入新值,若存在则对已有的值进行更新,例如在文本中统计词语出现的次数: ```rust use std::collections::HashMap; @@ -240,29 +240,29 @@ for word in text.split_whitespace() { println!("{:?}", map); ``` -上面代码中,新建一个`map`用于保存词语出现的次数,插入一个词语时会进行判断:若之前没有插入过,则使用该词语作Key,插入次数0,若之前插入过则取出之前统计的该词语出现的次数。 最后,对该词语出现的次数进行加一。 +上面代码中,新建一个 `map` 用于保存词语出现的次数,插入一个词语时会进行判断:若之前没有插入过,则使用该词语作Key,插入次数0作为Value,若之前插入过则取出之前统计的该词语出现的次数,对其加一。 -有两点值得注意: -- `or_insert`返回了`&mut v`引用,因此可以通过该可变引用直接修改`map`中对应的值 -- 使用`count`引用时,需要先进行解引用`*count`,否则会出现类型不匹配 +有两点值得注意: +- `or_insert` 返回了 `&mut v` 引用,因此可以通过该可变引用直接修改 `map` 中对应的值 +- 使用 `count` 引用时,需要先进行解引用 `*count`,否则会出现类型不匹配 ## 哈希函数 你肯定比较好奇,为何叫哈希表,到底什么是哈希。 -先来设想下,如果要实现`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,却查到了同一个结果,这种明显是错误的行为。 +但是问题又来了,如何保证不同 `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; @@ -275,6 +275,6 @@ hash.insert(42, "the answer"); assert_eq!(hash.get(&42), Some(&"the answer")); ``` -> 目前,`HashMap`使用的哈希函数是`SipHash`,它的性能不是很高,但是安全性很高。`SipHash`在中等大小的key上,性能相当不错,但是对于小型的key(例如整数)或者大型key(例如字符串)来说,性能还是不够好。若你需要极致性能,例如实现算法,可以考虑这个库:[ahash](https://github.com/tkaitchuck/ahash) +> 目前,`HashMap` 使用的哈希函数是 `SipHash`,它的性能不是很高,但是安全性很高。`SipHash` 在中等大小的 `Key` 上,性能相当不错,但是对于小型的 `Key` (例如整数)或者大型 `Key` (例如字符串)来说,性能还是不够好。若你需要极致性能,例如实现算法,可以考虑这个库:[ahash](https://github.com/tkaitchuck/ahash) -最后,如果你想要了解`HashMap`更多的用法,请参见本书的标准库解析章节:[HashMap常用方法](../../std/hashmap.md) \ No newline at end of file +最后,如果你想要了解 `HashMap` 更多的用法,请参见本书的标准库解析章节:[HashMap常用方法](../../std/hashmap.md) diff --git a/book/contents/basic/collections/intro.md b/book/contents/basic/collections/intro.md index f18292e7..4fc7a6b6 100644 --- a/book/contents/basic/collections/intro.md +++ b/book/contents/basic/collections/intro.md @@ -1,14 +1,13 @@ # 集合类型 -在Rust标准库中有这样一批原住民,它们天生贵族,当你看到的一瞬间,就能爱上它们, 上面是我瞎编的,其实主要是离了它们不行,不信等会我介绍后,你放个狠话,非它们不用试试? +在Rust标准库中有这样一批原住民,它们天生贵族,当你看到的一瞬间,就能爱上它们,上面是我瞎编的,其实主要是离了它们不行,不信等会我介绍后,你放个狠话,非它们不用试试? -集合在Rust中是一类比较特殊的类型,因为Rust中大多数数据类型都只能代表一个特定的值,但是它们可以代表一堆。而且与语言级别的数组、字符串类型不同,标准库里的这些家伙是分配在堆上,因此都可以进行动态的增加和减少。 +集合在Rust中是一类比较特殊的类型,因为Rust中大多数数据类型都只能代表一个特定的值,但是集合却可以代表一大堆值。而且与语言级别的数组、字符串类型不同,标准库里的这些家伙是分配在堆上,因此都可以进行动态的增加和减少。 -瞧,第一个集合排着整体的队列登场了,它里面的每个元素都雄赳赳气昂昂跟在另外一个元素后面,大小、宽度、高度竟然全部一致,真是令人惊叹。 它就是`Vector`类型,允许你创建一个动态数组,它里面的元素是一个紧挨着另一个排列的。 +瞧,第一个集合排着整体的队列登场了,它里面的每个元素都雄赳赳气昂昂跟在另外一个元素后面,大小、宽度、高度竟然全部一致,真是令人惊叹。 它就是 `Vector` 类型,允许你创建一个动态数组,它里面的元素是一个紧挨着另一个排列的。 -紧接着,第二个集合在全场的嘘声和羡慕眼光中闪亮登场,只见里面的元素排成一对一对的,彼此都手牵着手,非对方莫属,这种情深深雨蒙蒙的样子真是...挺欠扁的。 它就是`HashMap`类型,该类型允许你在里面存储`KV`对,每一个`K`都有唯一的`V`与之配对。 +紧接着,第二个集合在全场的嘘声和羡慕眼光中闪亮登场,只见里面的元素排成一对一对的,彼此都手牵着手,非对方莫属,这种情深深雨蒙蒙的样子真是...挺欠扁的。 它就是 `HashMap` 类型,该类型允许你在里面存储 `KV` 对,每一个 `K` 都有唯一的 `V` 与之配对。 -最后,请用热烈的掌声迎接我们的`String`集合,哦,抱歉,`String`集合天生低调,见不得前两个那样,因此被气走了,你可以去[这里](../compound-type/string-slice)找它. +最后,请用热烈的掌声迎接我们的 `String `集合,哦,抱歉,`String` 集合天生低调,见不得前两个那样,因此被气走了,你可以去[这里](../compound-type/string-slice)找它。 - -言归正传,本章所讲的`Vector`、`HashMap`再加上之前的`String`类型,是标准库中最最常用的集合类型,可以说,几乎任何一段代码中都可以找到它们的身影,那么先来看看`Vector`. \ No newline at end of file +言归正传,本章所讲的 `Vector`、`HashMap `再加上之前的 `String` 类型,是标准库中最最常用的集合类型,可以说,几乎任何一段代码中都可以找到它们的身影,那么先来看看`Vector`。 diff --git a/book/contents/basic/collections/vector.md b/book/contents/basic/collections/vector.md index 330d63d8..80dad041 100644 --- a/book/contents/basic/collections/vector.md +++ b/book/contents/basic/collections/vector.md @@ -2,7 +2,7 @@ 动态数组类型用`Vec`表示,事实上,在之前的章节,它的身影多次出现,我们一直没有细讲,只是简单的把它当作数组处理。 -动态数组允许你存储多个值,这些值在内存中一个紧挨着另一个排列,因此访问其中某个元素的成本非常低。动态数组只能存储相同类型的元素,如果你想存储不同类型的元素,可以使用之前讲过的枚举类型或者特征对象. +动态数组允许你存储多个值,这些值在内存中一个紧挨着另一个排列,因此访问其中某个元素的成本非常低。动态数组只能存储相同类型的元素,如果你想存储不同类型的元素,可以使用之前讲过的枚举类型或者特征对象。 总之,当我们想拥有一个列表,里面都是相同类型的数据时,动态数组将会非常有用。 @@ -10,41 +10,41 @@ 在Rust中,有多种方式可以创建动态数组。 #### Vec::new -使用`Vec::new`创建动态数组是最rusty的方式,它调用了`Vec`中的`new`关联函数: +使用 `Vec::new` 创建动态数组是最 rusty 的方式,它调用了 `Vec` 中的 `new` 关联函数: ```rust let v: Vec = Vec::new(); ``` -这里, `v`被显式地声明了类型`Vec`,这是因为Rust编译器无法从`Vec::new()`中得到任何关于类型的暗示信息,因此也无法推导出`v`的具体类型,但是当你向里面增加一个元素后,一切又不同了: +这里,`v` 被显式地声明了类型`Vec`,这是因为 Rust 编译器无法从 `Vec::new()` 中得到任何关于类型的暗示信息,因此也无法推导出 `v` 的具体类型,但是当你向里面增加一个元素后,一切又不同了: ```rust let mut v = Vec::new(); v.push(1); ``` -此时,`v`就无需手动声明类型,因为编译器通过`v.push(1)`,推测出`v`中的元素类型是`i32`,因此推导出`v`的类型是`Vec`. +此时,`v` 就无需手动声明类型,因为编译器通过 `v.push(1)`,推测出 `v` 中的元素类型是 `i32`,因此推导出 `v` 的类型是 `Vec`。 -> 如果预先知道要存储的元素个数,可以使用`Vec::with_capacity(capacity)`创建动态数组,这样可以避免因为插入大量新数据导致频繁的内存分配和拷贝,提升性能 +> 如果预先知道要存储的元素个数,可以使用 `Vec::with_capacity(capacity)` 创建动态数组,这样可以避免因为插入大量新数据导致频繁的内存分配和拷贝,提升性能 #### vec![] -还可以使用宏`vec!`来创建数组,与`Vec::new`有所不同,前者能在创建同时给予初始化值: +还可以使用宏 `vec!` 来创建数组,与 `Vec::new` 有所不同,前者能在创建同时给予初始化值: ```rust 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`方法: +向数组尾部添加元素,可以使用 `push` 方法: ```rust let mut v = Vec::new(); v.push(1); ``` -与其它类型一样,必须将`v`声明为`mut`后,才能进行修改。 +与其它类型一样,必须将 `v` 声明为 `mut` 后,才能进行修改。 ## Vector与其元素共存亡 -跟结构体一样,`Vector`类型在超出作用域范围后,会被自动删除: +跟结构体一样,`Vector` 类型在超出作用域范围后,会被自动删除: ```rust { let v = vec![1, 2, 3]; @@ -53,10 +53,10 @@ v.push(1); } // <- v超出作用域并在此处被删除 ``` -当`Vector`被删除后,它内部存储的所有内容也会随之被删除。目前来看,这种解决方案简单直白,但是当`vector`中的元素被引用后,事情可能会没那么简单。 +当 `Vector` 被删除后,它内部存储的所有内容也会随之被删除。目前来看,这种解决方案简单直白,但是当 `Vector` 中的元素被引用后,事情可能会没那么简单。 ## 从Vector中读取元素 -读取指定位置的元素有两种方式可选: 通过下标索引访问或者使用`get`方法: +读取指定位置的元素有两种方式可选:通过下标索引访问或者使用 `get` 方法: ```rust let v = vec![1, 2, 3, 4, 5]; @@ -69,10 +69,10 @@ match v.get(2) { } ``` -和其它语言一样,集合类型的索引下标都是从`0`开始,`&v[2]`表示借用`v`中的第三个元素,最终会获得该元素的引用。而`v.get(2)`也是访问第三个元素,但是有所不同的是,它返回了`Option<&T>`,因此还需要额外的`match`来匹配解构出具体的值。 +和其它语言一样,集合类型的索引下标都是从 `0` 开始,`&v[2]` 表示借用 `v` 中的第三个元素,最终会获得该元素的引用。而 `v.get(2)` 也是访问第三个元素,但是有所不同的是,它返回了 `Option<&T>`,因此还需要额外的 `match` 来匹配解构出具体的值。 -#### 下标索引与`.get`的区别 -这两种方式都能成功的读取到指定的数组元素,既然如此为什么会存在两种方法?何况`.get`还会增加使用复杂度,让我们通过示例说明: +#### 下标索引与 `.get` 的区别 +这两种方式都能成功的读取到指定的数组元素,既然如此为什么会存在两种方法?何况 `.get` 还会增加使用复杂度,让我们通过示例说明: ```rust let v = vec![1, 2, 3, 4, 5]; @@ -80,11 +80,11 @@ let does_not_exist = &v[100]; let does_not_exist = v.get(100); ``` -运行以上代码,`&v[100]`的访问方式会导致程序无情报错退出,因为发生了数组越界访问。 但是`v.get`就不会,它在内部做了处理,有值的时候返回`Some(T)`,无值的时候返回`None`,因此`v.get`的使用方式非常安全。 +运行以上代码,`&v[100]` 的访问方式会导致程序无情报错退出,因为发生了数组越界访问。 但是 `v.get` 就不会,它在内部做了处理,有值的时候返回 `Some(T)`,无值的时候返回 `None`,因此 `v.get` 的使用方式非常安全。 -既然如此,为何不统一使用`v.get`的形式?因为实在是有些啰嗦,Rust语言的设计者和使用者在审美这方面还是相当统一的:简洁即正义,何况性能上也会有轻微的损耗。 +既然如此,为何不统一使用 `v.get` 的形式?因为实在是有些啰嗦,Rust语言的设计者和使用者在审美这方面还是相当统一的:简洁即正义,何况性能上也会有轻微的损耗。 -既然有两个选择,肯定就有如何选择的问题,答案很简单,当你确保索引不会越界的时候,就用索引访问,否则用`.get`。例如,访问第几个数组元素并不取决于我们,而是取决于用户的输入时,用`.get`会非常适合,天知道那些可爱的用户会输入一个什么样的数字进来! +既然有两个选择,肯定就有如何选择的问题,答案很简单,当你确保索引不会越界的时候,就用索引访问,否则用 `.get`。例如,访问第几个数组元素并不取决于我们,而是取决于用户的输入时,用 `.get` 会非常适合,天知道那些可爱的用户会输入一个什么样的数字进来! ##### 同时借用多个数组元素 既然涉及到借用数组元素,那么很可能会遇到同时借用多个数组元素的情况,还记得在[所有权和借用](../ownership/borrowing.md#借用规则总结)章节咱们讲过的借用规则嘛?如果记得,就来看看下面的代码:) @@ -98,9 +98,9 @@ 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`后被使用了,那么妥妥的,编译器就会报错: +可是上面的代码中,`first` 这个不可变借用在可变借用 `v.push` 后被使用了,那么妥妥的,编译器就会报错: ```console $ cargo run Compiling collections v0.1.0 (file:///projects/collections) @@ -122,15 +122,15 @@ error: could not compile `collections` due to previous error 其实,按理来说,这两个引用不应该互相影响的:一个是查询元素,一个是在数组尾部插入元素,完全不相干的操作,为何编译器要这么严格呢? -原因在于:数组的大小是可变的,当老数组的大小不够用时,Rust会重新分配一块更大的内存空间,然后把老数组拷贝过来。这种情况下,之前的引用显然会指向一块无效的内存,这非常rusty - 对用户进行严格的教育。 +原因在于:数组的大小是可变的,当旧数组的大小不够用时,Rust会重新分配一块更大的内存空间,然后把旧数组拷贝过来。这种情况下,之前的引用显然会指向一块无效的内存,这非常rusty - 对用户进行严格的教育。 -其实想想,**在长大之后,我们感激人生路上遇到过的严师益友,正是因为他们,我们才在正确的道路上不断前行,虽然在那个时候,并不能理解他们**,而Rust就如那个良师益友,它不断的在纠正我们不好的编程习惯,直到某一天,你发现自己能写出一次性通过的漂亮代码时,就能明白它的良苦用心。 +其实想想,**在长大之后,我们感激人生路上遇到过的严师益友,正是因为他们,我们才在正确的道路上不断前行,虽然在那个时候,并不能理解他们**,而 Rust 就如那个良师益友,它不断的在纠正我们不好的编程习惯,直到某一天,你发现自己能写出一次性通过的漂亮代码时,就能明白它的良苦用心。 > 若读者想要更深入的了解`Vec`,可以看看[Rustonomicon],其中从零手撸一个动态数组,非常适合深入学习 ## 迭代遍历Vector中的元素 -如果想要依次访问数组中的元素,可以使用迭代的方式去遍历数组,这种方式比用下标的方式去遍历数组更安全也更高效(每次下标访问都会触发数组边界检查): +如果想要依次访问数组中的元素,可以使用迭代的方式去遍历数组,这种方式比用下标的方式去遍历数组更安全也更高效(每次下标访问都会触发数组边界检查): ```rust let v = vec![1, 2, 3]; for i in &v { @@ -138,7 +138,7 @@ for i in &v { } ``` -也可以在迭代过程中,修改`Vector`中的元素: +也可以在迭代过程中,修改 `Vector` 中的元素: ```rust let mut v = vec![1, 2, 3]; for i in &mut v { @@ -147,7 +147,7 @@ for i in &mut v { ``` ## 存储不同类型的元素 -在本节开头,有讲到数组的元素必需类型相同,但是也提到了解决方案: 那就是通过使用枚举类型和特征对象来实现不同类型元素的存储。先来看看通过枚举如何实现: +在本节开头,有讲到数组的元素必需类型相同,但是也提到了解决方案:那就是通过使用枚举类型和特征对象来实现不同类型元素的存储。先来看看通过枚举如何实现: ```rust #[derive(Debug)] enum IpAddr { @@ -170,7 +170,7 @@ fn show_addr(ip: IpAddr) { } ``` -数组`v`中存储了两种不同的`ip`地址,但是这两种都属于`IpAddr`枚举类型的成员,因此可以存储在数组中。 +数组 `v` 中存储了两种不同的 `ip` 地址,但是这两种都属于 `IpAddr` 枚举类型的成员,因此可以存储在数组中。 再来看看特征对象的实现: ```rust @@ -203,8 +203,8 @@ fn main() { } ``` -比枚举实现要稍微复杂一些,我们为`V4`和`V6`都实现了特征`IpAddr`,然后将它俩的实例用`Box::new`包裹后,存在了数组`v`中,需要注意的是,这里必需手动的指定类型:`Vec>`,表示数组`v`存储的是特征`IpAddr`的对象,这样就实现了在数组中存储不同的类型. +比枚举实现要稍微复杂一些,我们为 `V4` 和 `V6` 都实现了特征 `IpAddr`,然后将它俩的实例用 `Box::new` 包裹后,存在了数组 `v` 中,需要注意的是,这里必需手动的指定类型:`Vec>`,表示数组 `v` 存储的是特征 `IpAddr` 的对象,这样就实现了在数组中存储不同的类型。 在实际使用场景中,特征对象数组要比枚举数组常见很多,主要原因在于[特征对象非常灵活](../trait/trait-object.md),而编译器对枚举的限制较多,且无法动态增加类型。 -最后,如果你想要了解`Vector`更多的用法,请参见本书的标准库解析章节:[`Vector`常用方法](../../std/vector.md) \ No newline at end of file +最后,如果你想要了解 `Vector `更多的用法,请参见本书的标准库解析章节:[`Vector`常用方法](../../std/vector.md) diff --git a/book/contents/basic/converse.md b/book/contents/basic/converse.md index 925532c8..25916e62 100644 --- a/book/contents/basic/converse.md +++ b/book/contents/basic/converse.md @@ -1,6 +1,6 @@ # 类型转换 -Rust是类型安全的语言,因此在Rust中做类型转换不是一件简单的事,这一章节我们将对Rust中的类型转换进行详尽讲解。 +Rust 是类型安全的语言,因此在 Rust 中做类型转换不是一件简单的事,这一章节我们将对Rust中的类型转换进行详尽讲解。 ## `as`转换 先来看一段代码: @@ -15,24 +15,24 @@ fn main() { } ``` -能跟着这本书一直学习到这里,说明你对Rust已经有了一定的理解,那么一眼就能看出这段代码注定会报错,因为`a`和`b`拥有不同的类型,Rust不允许两种不同的类型进行比较。 +能跟着这本书一直学习到这里,说明你对 Rust 已经有了一定的理解,那么一眼就能看出这段代码注定会报错,因为 `a` 和 `b` 拥有不同的类型,Rust 不允许两种不同的类型进行比较。 -解决办法很简单,只要把`b`转换成`i32`类型即可,这里使用`as`操作符来完成:`if a < (b as i32) {...}`. 那么为什么不把`a`转换成`u16`类型呢? +解决办法很简单,只要把 `b` 转换成 `i32` 类型即可,Rust中内置了一些基本类型之间的转换,这里使用 `as` 操作符来完成:`if a < (b as i32) {...}`。那么为什么不把 `a` 转换成 `u16` 类型呢? -因为每个类型能表达的大小不一样,如果把大的类型转换成小的类型,会造成错误, 因此我们需要把小的类型转换成大的类型,来避免这些问题的发生. +因为每个类型能表达的数据范围不同,如果把范围较大的类型转换成较小的类型,会造成错误,因此我们需要把范围较小的类型转换成较大的类型,来避免这些问题的发生。 -> 使用类型转换需要小心,因为如果执行以下操作`300_i32 as i8`,你将获得`44`这个值,而不是`300`,因为`i8`类型能表达的的最大值为`2^7 - 1`, 使用以下代码可以查看`i8`的最大值: +> 使用类型转换需要小心,因为如果执行以下操作 `300_i32 as i8`,你将获得 `44` 这个值,而不是 `300`,因为 `i8` 类型能表达的的最大值为 `2^7 - 1`,使用以下代码可以查看 `i8` 的最大值: ```rust let a = i8::MAX; println!("{}",a); ``` -下面列出了常用的转换形式: +下面列出了常用的转换形式: ```rust fn main() { let a = 3.1 as i8; let b = 100_i8 as i32; - let c = 'a' as u8; // 将字符'a'转换为整数, 97 + let c = 'a' as u8; // 将字符'a'转换为整数,97 println!("{},{},{}",a,b,c) } @@ -43,7 +43,7 @@ fn main() { let mut values: [i32; 2] = [1, 2]; let p1: *mut i32 = values.as_mut_ptr(); let first_address = p1 as usize; // 将p1内存地址转换为一个整数 -let second_address = first_address + 4; // 4 == std:mem::size_of::(), i32类型占用4个字节,因此将内存地址 + 4 +let second_address = first_address + 4; // 4 == std:mem::size_of::(),i32类型占用4个字节,因此将内存地址 + 4 let p2 = second_address as *mut i32; // 访问该地址指向的下一个整数p2 unsafe { *p2 += 1; @@ -55,17 +55,17 @@ assert_eq!(values[1], 3); 1. 数组切片原生指针之间的转换,不会改变数组占用的内存字节数,尽管数组元素的类型发生了改变: ```rust fn main() { - let a: *const [u16] = &[1,2,3,4,5]; - let b = a as *const[u8]; - assert_eq!(std::mem::size_of_val(&a),std::mem::size_of_val(&b)) + let a: *const [u16] = &[1, 2, 3, 4, 5]; + let b = a as *const [u8]; + assert_eq!(std::mem::size_of_val(&a), std::mem::size_of_val(&b)) } ``` 2. 转换不具有传递性 -就算`e as U1 as U2`是合法的,也不能说明`e as U2`是合法的。 +就算 `e as U1 as U2` 是合法的,也不能说明 `e as U2` 是合法的(`e` 不能直接转换成 `U2`)。 ## TryInto转换 -在一些场景中,使用`as`关键字会有比较大的限制,因为你想要在类型转换上拥有完全的控制,例如处理转换错误,那么你将需要`TryInto`: +在一些场景中,使用 `as` 关键字会有比较大的限制。如果你想要在类型转换上拥有完全的控制而不依赖内置的转换,例如处理转换错误,那么可以使用 `TryInto`: ```rust use std::convert::TryInto; @@ -82,11 +82,11 @@ fn main() { } ``` -上面代码中引入了`std::convert::TryInto`特征,但是却没有使用它,可能有些同学会为此困惑,主要原因在于**如果你要使用一个特征的方法,那么你需要引入该特征到当前的作用域中**,我们在上面用到了`try_into`方法,因此需要引入对应的特征。但是Rust又提供了一个非常便利的办法,把最常用的标准库中的特征通过[`std::prelude`](std::convert::TryInto)模块提前引入到当前作用域中,其中包括了`std::convert::TryInto`,你可以尝试删除第一行的代码`use ...`,看看是否会报错. +上面代码中引入了 `std::convert::TryInto` 特征,但是却没有使用它,可能有些同学会为此困惑,主要原因在于**如果你要使用一个特征的方法,那么你需要引入该特征到当前的作用域中**,我们在上面用到了 `try_into` 方法,因此需要引入对应的特征。但是 Rust 又提供了一个非常便利的办法,把最常用的标准库中的特征通过[`std::prelude`](std::convert::TryInto)模块提前引入到当前作用域中,其中包括了 `std::convert::TryInto`,你可以尝试删除第一行的代码 `use ...`,看看是否会报错。 -`try_into`会尝试进行一次转换,如果失败,则会返回一个`Result`,然后你可以进行相应的错误处理,但是因为我们的例子只是为了快速测试,因此使用了`unwrap`方法,该方法在发现错误时,会直接调用`panic`导致程序的崩溃退出,在实际项目中,请不要这么使用,具体见[panic](./exception-error.md#panic)部分. +`try_into` 会尝试进行一次转换,如果失败,则会返回一个 `Result`,然后你可以进行相应的错误处理,但是因为我们的例子只是为了快速测试,因此使用了 `unwrap` 方法,该方法在发现错误时,会直接调用 `panic` 导致程序的崩溃退出,在实际项目中,请不要这么使用,具体见[panic](./exception-error.md#panic)部分。 -最主要的是`try_into`转换会捕获大类型向小类型转换时导致的溢出错误: +最主要的是 `try_into` 转换会捕获大类型向小类型转换时导致的溢出错误: ```rust fn main() { let b: i16 = 1500; @@ -100,11 +100,11 @@ fn main() { }; } ``` -运行后输出如下`"out of range integral type conversion attempted"`, 在这里我们程序捕获了错误,编译器告诉我们类型范围超出的转换是不被允许的,因为我们试图把`1500_i16`转换为`u8`类型,后者明显不足以承载这么大的值。 +运行后输出如下 `"out of range integral type conversion attempted"`,在这里我们程序捕获了错误,编译器告诉我们类型范围超出的转换是不被允许的,因为我们试图把 `1500_i16` 转换为 `u8` 类型,后者明显不足以承载这么大的值。 ## 通用类型转换 -虽然`as`和`TryInto`很强大,但是只能应用在数值类型上,可是Rust有如此多的类型,想要为这些类型实现转换,我们需要另谋出路,先来看看在一个笨办法,将一个结构体转换为另外一个结构体: +虽然 `as` 和 `TryInto` 很强大,但是只能应用在数值类型上,可是 Rust 有如此多的类型,想要为这些类型实现转换,我们需要另谋出路,先来看看在一个笨办法,将一个结构体转换为另外一个结构体: ```rust struct Foo { x: u32, @@ -125,9 +125,9 @@ fn reinterpret(foo: Foo) -> Bar { 简单粗暴,但是从另外一个角度来看,也挺啰嗦的,好在Rust为我们提供了更通用的方式来完成这个目的。 #### 强制类型转换 -在某些情况下,类型是可以进行隐式强制转换的,但是这些转换其实弱化了Rust的类型系统,它们的存在是为了让Rust在大多数场景可以工作(说白了,帮助用户省事),而不是报各种类型上的编译错误。 +在某些情况下,类型是可以进行隐式强制转换的,虽然这些转换弱化了 Rust 的类型系统,但是它们的存在是为了让Rust在大多数场景可以工作(说白了,帮助用户省事),而不是报各种类型上的编译错误。 -首先,在匹配特征时,不会做任何强制转换(除了方法)。如果有一个类型`T`可以强制转换为`U`,不代表`impl T`可以强制转换为`impl U`,例如以下的代码就无法通过编译检查: +首先,在匹配特征时,不会做任何强制转换(除了方法)。一个类型 `T` 可以强制转换为 `U`,不代表 `impl T` 可以强制转换为 `impl U`,例如下面的代码就无法通过编译检查: ```rust trait Trait {} @@ -154,16 +154,16 @@ error[E0277]: the trait bound `&mut i32: Trait` is not satisfied = note: `Trait` is implemented for `&i32`, but not for `&mut i32` ``` -`&i32`实现了特征`Trait`,`&mut i32`可以转换为`&i32`,但是`&mut i32`依然无法作为`Trait`来使用。 +`&i32`实现了特征`Trait`,`&mut i32`可以转换为`&i32`,但是`&mut i32`依然无法作为`Trait`来使用。 #### 点操作符 方法调用的点操作符看起来简单,实际上非常不简单,它在调用时,会发生很多魔法般的类型转换,例如:自动引用、自动解引用,强制类型转换直到类型能匹配等。 -假设有一个方法`foo`,它有一个接收器(接收器就是`self`、`&self`、`&mut self`参数)。如果调用`value.foo()`,编译器在调用`foo`之前,需要决定到底使用哪个`Self`类型来调用。现在假设`value`拥有类型`T`. +假设有一个方法 `foo`,它有一个接收器(接收器就是 `self`、`&self`、`&mut self` 参数)。如果调用 `value.foo()`,编译器在调用 `foo` 之前,需要决定到底使用哪个 `Self` 类型来调用。现在假设 `value` 拥有类型 `T`。 再进一步,我们使用[完全限定语法](https://course.rs/basic/trait/advance-trait.html#完全限定语法)来进行准确的函数调用: -1. 首先,编译器检查它是否可以直接调用`T::foo(value)`, 称之为**值方法调用** -2. 如果上一步调用无法完成(例如方法类型错误或者特征没有针对`Self`进行实现,上文提到过特征不能进行强制转换),那么编译器会尝试增加自动引用,以为着编译器会尝试以下调用:`<&T>::foo(value)`和`<&mut T>::foo(value)`, 称之为**引用方法调用** +1. 首先,编译器检查它是否可以直接调用`T::foo(value)`,称之为**值方法调用** +2. 如果上一步调用无法完成(例如方法类型错误或者特征没有针对 `Self` 进行实现,上文提到过特征不能进行强制转换),那么编译器会尝试增加自动引用,以为着编译器会尝试以下调用:`<&T>::foo(value)`和`<&mut T>::foo(value)`,称之为**引用方法调用** 3. 若上面两个方法依然不工作,编译器会试着解引用`T`,然后再进行尝试。这里使用了`Deref`特征 - 若`T: Deref`(`T`可以被解引用为`U`),那么编译器会使用`U`类型进行尝试,称之为**解引用方法调用** 4. 若`T`不能被解引用,且`T`是一个定长类型(在编译器类型长度是已知的),那么编译器也会尝试将`T`从定长类型转为不定长类型,例如将`[i32; 2]`转为`[i32]` 5. 若还是不行,那...没有那了,最后编译器大喊一声:汝欺我甚,不干了! @@ -175,10 +175,10 @@ let first_entry = array[0]; ``` `array`数组的底层数据隐藏在了重重封锁之后,那么编译器如何使用`array[0]`这种数组原生访问语法通过重重封锁,准确的访问到数组中的第一个元素? -1. 首先,`array[0]`只是[`Index`](https://doc.rust-lang.org/std/ops/trait.Index.html)特征的语法糖: 编译器会将`array[0]`转换为`array.index(0)`调用, 当然在调用之前,编译器会先检查`array`是否实现了`Index`特征. +1. 首先,`array[0]`只是[`Index`](https://doc.rust-lang.org/std/ops/trait.Index.html)特征的语法糖: 编译器会将`array[0]`转换为`array.index(0)`调用,当然在调用之前,编译器会先检查`array`是否实现了`Index`特征. 2. 接着,编译器检查`Rc>`是否有否实现`Index`特征,结果是否,不仅如此,`&Rc> `与`&mut Rc>`也没有实现. 3. 上面的都不能工作,编译器开始对`Rc>`进行解引用,把它转变成`Box<[T; 3]>` -4. 此时继续对`Box<[T; 3]>`进行上面的操作:`Box<[T; 3]>`, `&Box<[T; 3]>`, and `&mut Box<[T; 3]>`都没有实现`Index`特征,所以编译器开始对`Box<[T; 3]>`进行解引用,然后我们得到了`[T; 3]` +4. 此时继续对`Box<[T; 3]>`进行上面的操作:`Box<[T; 3]>`,`&Box<[T; 3]>`,和`&mut Box<[T; 3]>`都没有实现`Index`特征,所以编译器开始对`Box<[T; 3]>`进行解引用,然后我们得到了`[T; 3]` 5. `[T; 3]`以及它的各种引用都没有实现`Index`索引(是不是很反直觉:D,在直觉中,数组都可以通过索引访问,实际上只有数组切片才可以!),它也不能再进行解引用,因此编译器只能祭出最后的大杀器:将定长转为不定长,因此`[T; 3]`被转换成`[T]`,也就是数组切片,它实现了`Index`特征,因此最终我们可以通过`index`方法访问到对应的元素. 过程看起来很复杂,但是也还好挺好理解,如果你先不能彻底理解,也不要紧,等以后对Rust理解更深了,同时需要深入理解类型转换时,再来细细品读本章。 @@ -189,7 +189,7 @@ fn do_stuff(value: &T) { let cloned = value.clone(); } ``` -上面例子中`cloned`的类型时什么?首先编译器检查能不能进行**值方法调用**, `value`的类型是`&T`,同时`clone`方法的签名也是`&T`: `fn clone(&T) -> T`,因此可以进行值方法调用, 再加上编译器知道了`T`实现了`Clone`,因此`cloned`的类型是`T`. +上面例子中`cloned`的类型时什么?首先编译器检查能不能进行**值方法调用**,`value`的类型是`&T`,同时`clone`方法的签名也是`&T`: `fn clone(&T) -> T`,因此可以进行值方法调用,再加上编译器知道了`T`实现了`Clone`,因此`cloned`的类型是`T`。 如果`T: Clone`的特征约束被移除呢? ```rust @@ -200,7 +200,7 @@ fn do_stuff(value: &T) { 首先,从直觉上来说,该方法会报错,因为`T`没有实现`Clone`特征,但是真实情况是什么呢? -我们先来推导一番。 首先通过值方法调用就不再可行,因此`T`没有实现`Clone`特征,也就无法调用`T`的`clone`方法。接着编译器尝试**引用方法调用**,此时`T`变成`&T`,在这种情况下,`clone`方法的签名如下:`fn clone(&&T) -> &T`, 记着我们现在对`value`进行了引用。 编译器发现`&T`实现了`Clone`类型(所有的引用类型都可以被复制,因为其实就是复制一份地址),因此可以可以推出`cloned`也是`&T`类型。 +我们先来推导一番。 首先通过值方法调用就不再可行,因此`T`没有实现`Clone`特征,也就无法调用`T`的`clone`方法。接着编译器尝试**引用方法调用**,此时`T`变成`&T`,在这种情况下,`clone`方法的签名如下:`fn clone(&&T) -> &T`,记着我们现在对`value`进行了引用。 编译器发现`&T`实现了`Clone`类型(所有的引用类型都可以被复制,因为其实就是复制一份地址),因此可以可以推出`cloned`也是`&T`类型。 最终,我们复制出一份引用指针,这很合理,因为值类型`T`没有实现`Clone`,只能去复制一个指针了。 @@ -215,13 +215,13 @@ fn clone_containers(foo: &Container, bar: &Container) { } ``` -推断下上面的`foo_cloned`和`bar_cloned`是什么类型?提示: 关键在`Container`的泛型参数,一个是`i32`的具体类型,一个是泛型类型,其中`i32`实现了`Clone`,但是`T`并没有. +推断下上面的`foo_cloned`和`bar_cloned`是什么类型?提示: 关键在`Container`的泛型参数,一个是`i32`的具体类型,一个是泛型类型,其中`i32`实现了`Clone`,但是`T`并没有. 首先要复习一下复杂类型派生`Clone`的规则:一个复杂类型能否派生`Clone`,需要它内部的所有子类型都能进行`Clone`。因此`Container(Arc)`是否实现`Clone`的关键在于`T`类型是否实现了`Clone`. -上面代码中,`Container`实现了`Clone`特征,因此编译器可以直接进行值方法调用,此时相当于直接调用`foo.clone`,其中`clone`的函数签名是`fn clone(&T) -> T`,由此可以看出`foo_cloned`的类型是`Container`. +上面代码中,`Container`实现了`Clone`特征,因此编译器可以直接进行值方法调用,此时相当于直接调用`foo.clone`,其中`clone`的函数签名是`fn clone(&T) -> T`,由此可以看出`foo_cloned`的类型是`Container`. -然而,`bar_cloned`的类型却是`&Container`.这个不合理啊,明明我们为`Container`派生了`Clone`特征,因此它也应该是`Container`类型才对。万事皆有因,我们先来看下`derive`宏最终生成的代码大概是啥样的: +然而,`bar_cloned`的类型却是`&Container`.这个不合理啊,明明我们为`Container`派生了`Clone`特征,因此它也应该是`Container`类型才对。万事皆有因,我们先来看下`derive`宏最终生成的代码大概是啥样的: ```rust impl Clone for Container where T: Clone { fn clone(&self) -> Self { @@ -232,7 +232,7 @@ impl Clone for Container where T: Clone { 从上面代码可以看出,派生`Clone`能实现的[根本是`T`实现了`Clone`特征](https://doc.rust-lang.org/std/clone/trait.Clone.html#derivable):`where T: Clone`, 因此`Container`就没有实现`Clone`特征。 -编译器接着会去尝试引用方法调用,此时`&Container`引用实现了`Clone`,最终可以得出`bar_cloned`的类型是`&Container`, +编译器接着会去尝试引用方法调用,此时`&Container`引用实现了`Clone`,最终可以得出`bar_cloned`的类型是`&Container`。 当然,也可以为`Container`手动实现`Clone`特征: ```rust @@ -251,7 +251,7 @@ impl Clone for Container { 前方危险,敬请绕行! -类型系统,你让开!我要自己转换这些类型,不成功便成仁!虽然本书大多是关于安全的内容,我还是希望你能仔细考虑避免使用本章讲到的内容。这是你在 Rust 中所能做到的真真正正、彻彻底底、最最可怕的非安全行为, 在这里,所有的保护机制都形同虚设。 +类型系统,你让开!我要自己转换这些类型,不成功便成仁!虽然本书大多是关于安全的内容,我还是希望你能仔细考虑避免使用本章讲到的内容。这是你在 Rust 中所能做到的真真正正、彻彻底底、最最可怕的非安全行为,在这里,所有的保护机制都形同虚设。 先让你看看深渊长什么样,开开眼,然后你再决定是否深入: `mem::transmute`将类型`T`直接转成类型`U`,唯一的要求就是,这两个类型占用同样大小的字节数!我的天,这也算限制?这简直就是无底线的转换好吧?看看会导致什么问题: 1. 首先也是最重要的,转换后创建一个任意类型的实例会造成无法想象的混乱,而且根本无法预测。不要把`3`转换成`bool`类型,就算你根本不会去使用该`bool`类型,也不要去这样转换。 @@ -265,7 +265,7 @@ impl Clone for Container { 对于第5条,你该如何知道内存的排列布局是一样的呢?对于`repr(C)`类型和`repr(transparent)`类型来说,它们的布局是有着精确定义的。但是对于你自己的"普通却自信"的Rust类型`repr(Rust)`来说,它可不是有着精确定义的。甚至同一个泛型类型的不同实例都可以有不同的内存布局。`Vec`和`Vec`它们的字段可能有着相同的顺序,也可能没有。对于数据排列布局来说,什么能保证,什么不能保证目前还在Rust开发组的[工作任务](https://rust-lang.github.io/unsafe-code-guidelines/layout.html)中呢. -你以为你之前凝视的是深渊吗?不,你凝视的只是深渊的大门。`mem::transmute_copy`才是真正的深渊,它比之前的还要更加危险和不安全。它从`T`类型中拷贝出`U`类型所需的字节数,然后转换成`U`。`mem::transmute`尚有大小检查,能保证两个数据的内存大小一致,现在这哥们干脆连这个也丢了,只不过`U`的尺寸若是比`T`大,会是一个未定义行为。 +你以为你之前凝视的是深渊吗?不,你凝视的只是深渊的大门。`mem::transmute_copy`才是真正的深渊,它比之前的还要更加危险和不安全。它从`T`类型中拷贝出`U`类型所需的字节数,然后转换成`U`。`mem::transmute`尚有大小检查,能保证两个数据的内存大小一致,现在这哥们干脆连这个也丢了,只不过`U`的尺寸若是比`T`大,会是一个未定义行为。 当然,你也可以通过原生指针转换和`unions`(todo!)获得所有的这些功能,但是你将无法获得任何编译提示或者检查。原生指针转换和`unions`也不是魔法,无法逃避上面说的规则。 diff --git a/book/contents/basic/result-error/intro.md b/book/contents/basic/result-error/intro.md index e86b8fa8..5ed28f00 100644 --- a/book/contents/basic/result-error/intro.md +++ b/book/contents/basic/result-error/intro.md @@ -1,16 +1,16 @@ # 返回和错误处理 -飞鸽传书、八百里加急,自古以来,掌权者最需要的都是及时获得对某个事物的信息反馈,在此过程中,也定义了相应的应急处理措施。 +飞鸽传书、八百里加急,自古以来,掌权者最需要的就是及时获得对某个事物的信息反馈,在此过程中,也定义了相应的应急处理措施。 社会演变至今,这种思想依然没变,甚至来到计算中的微观世界,也是如此。及时、准确的获知系统在发生什么,是程序设计的重中之重。因此能够准确的分辨函数返回值是正确的还是错误的、以及在发生错误时该怎么快速处理,成了程序设计语言的必备功能。 -Go语言为人诟病的其中一点就是`if err != nil {}`的大量使用,缺乏一些程序设计的美感,不过我倒是觉得这种简单的方式也有其好处,就是阅读代码时的流畅感很强,你不需要过多的思考各种语法是什么意思。与Go语言不同,Rust博采众家之长,整出了颇具自身色彩的返回值和错误处理体系,本章我们就从高屋建瓴的角度来学习,更加深入的讲解见[此章](../../errors/intro.md). +Go 语言为人诟病的其中一点就是 `if err != nil {}` 的大量使用,缺乏一些程序设计的美感,不过我倒是觉得这种简单的方式也有其好处,就是阅读代码时的流畅感很强,你不需要过多的思考各种语法是什么意思。与 Go 语言不同,Rust 博采众家之长,实现了颇具自身色彩的返回值和错误处理体系,本章我们就高屋建瓴地来学习,更加深入的讲解见[此章](../../errors/intro.md). ## Rust的错误哲学 -错误对于软件来说是不可避免的,因此一门优秀的编程语言必须有其完整的错误处理哲学。在很多情况下,Rust需要你承认自己的代码可能会出错,并提前采取行动,来处理这些错误。 +错误对于软件来说是不可避免的,因此一门优秀的编程语言必须有其完整的错误处理哲学。在很多情况下,Rust 需要你承认自己的代码可能会出错,并提前采取行动,来处理这些错误。 -Rust中的错误主要分为两类: -- **可恢复错误**, 通常用于从系统全局角度来看可以接受的错误,例如处理用户的访问、操作等错误,这些错误只会影响某个用户自身的操作进程,而不会对系统的全局稳定性产生影响 -- **不可恢复错误**,刚好相反,该错误通常是全局性或者系统性的错误,例如数组越界访问,系统启动时发生了影响启动流程的错误等等,这些错误的影响往往对于系统来说是致命的 +Rust 中的错误主要分为两类: +- **可恢复错误**,通常用于从系统全局角度来看可以接受的错误,例如处理用户的访问、操作等错误,这些错误只会影响某个用户自身的操作进程,而不会对系统的全局稳定性产生影响 +- **不可恢复错误**,刚好相反,该错误通常是全局性或者系统性的错误,例如数组越界访问,系统启动时发生了影响启动流程的错误等等,这些错误的影响往往对于系统来说是致命的 -很多编程语言,并不会区分这些错误,而是直接采用异常的方式去处理。Rust没有异常,但是Rust也有自己的卧龙凤雏:`Result`用于可恢复错误,`panic!`用于不可恢复错误。 \ No newline at end of file +很多编程语言,并不会区分这些错误,而是直接采用异常的方式去处理。Rust没有异常,但是Rust也有自己的卧龙凤雏:`Result` 用于可恢复错误,`panic!` 用于不可恢复错误。 diff --git a/book/contents/basic/result-error/panic.md b/book/contents/basic/result-error/panic.md index 0fa076b1..ba599941 100644 --- a/book/contents/basic/result-error/panic.md +++ b/book/contents/basic/result-error/panic.md @@ -1,16 +1,16 @@ # panic深入剖析 -在正式开始之前,先来思考一个问题: 假设我们想要从文件读取数据,如果失败,你有没有好的办法通知调用者为何失败?如果成功,你有没有好的办法把读取的结果返还给调用者? +在正式开始之前,先来思考一个问题:假设我们想要从文件读取数据,如果失败,你有没有好的办法通知调用者为何失败?如果成功,你有没有好的办法把读取的结果返还给调用者? ## panic!与不可恢复错误 -上面的问题在真实场景,其实挺复杂的,让我们先做一个假设:文件读取操作发生在系统启动阶段。那么可以轻易得出一个结论,一旦文件读取失败,那么系统启动也将失败,这意味着该失败是不可恢复的错误,无论是因为文件不存在还是操作系统硬盘的问题,这些只是错误的原因不同,但是归根到底都是不可恢复的错误(梳理清楚当前场景的错误类型非常重要). +上面的问题在真实场景会经常遇到,其实处理起来挺复杂的,让我们先做一个假设:文件读取操作发生在系统启动阶段。那么可以轻易得出一个结论,一旦文件读取失败,那么系统启动也将失败,这意味着该失败是不可恢复的错误,无论是因为文件不存在还是操作系统硬盘的问题,这些只是错误的原因不同,但是归根到底都是不可恢复的错误(梳理清楚当前场景的错误类型非常重要)。 -既然是不可恢复错误,那么一旦发生,只需让程序崩溃即可。对此,Rust为我们提供了`panic!`宏,当调用执行该宏时,**程序会打印出一个错误信息,展开报错点往前的函数调用堆栈,最后退出程序**. +既然是不可恢复错误,那么一旦发生,只需让程序崩溃即可。对此,Rust 为我们提供了 `panic!` 宏,当调用执行该宏时,**程序会打印出一个错误信息,展开报错点往前的函数调用堆栈,最后退出程序**。 -切记,一定是不可恢复的错误,才调用`panic!`处理,你总不想系统仅仅因为用户随便传入一个非法参数就崩溃吧?所以,**只有当你不知道该如何处理时,再去调用panic!**. +切记,一定是不可恢复的错误,才调用 `panic!` 处理,你总不想系统仅仅因为用户随便传入一个非法参数就崩溃吧?所以,**只有当你不知道该如何处理时,再去调用panic!**. ## 调用panic! -首先,来调用一下`panic!`,这里使用了最简单的代码实现,实际上你在程序的任何地方都可以这样调用: +首先,来调用一下 `panic!`,这里使用了最简单的代码实现,实际上你在程序的任何地方都可以这样调用: ```rust fn main() { panic!("crash and burn"); @@ -23,9 +23,9 @@ thread 'main' panicked at 'crash!!1', src/main.rs:3:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ``` -以上信息包含了两条重要信息: -- `main`函数所在的线程崩溃了,发生的代码位置是`src/main.rs`中的第3行第5个字符(去除该行前面的空字符) -- 在使用时加上一个环境变量可以获取更详细的栈展开信息: `RUST_BACKTRACE=1 cargo run` +以上信息包含了两条重要信息: +- `main` 函数所在的线程崩溃了,发生的代码位置是 `src/main.rs` 中的第3行第5个字符(去除该行前面的空字符) +- 在使用时加上一个环境变量可以获取更详细的栈展开信息:`RUST_BACKTRACE=1 cargo run` 下面让我们针对第二点进行详细展开讲解。 @@ -40,17 +40,17 @@ fn main() { ``` 上面的代码很简单,数组只有`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 ``` -好的,现在成功知道问题发生的位置,但是如果我们想知道该问题之前经过了哪些调用环节,该怎么办?那就按照提示使用`RUST_BACKTRACE=1 cargo run`来再一次运行程序: +好的,现在成功知道问题发生的位置,但是如果我们想知道该问题之前经过了哪些调用环节,该怎么办?那就按照提示使用 `RUST_BACKTRACE=1 cargo run` 来再一次运行程序: ```console thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5 stack backtrace: @@ -73,87 +73,87 @@ stack backtrace: note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace. ``` -上面的代码就是一次栈展开(也称栈回溯),它包含了函数调用的顺序,当然按照逆序排列:最近调用的函数排在列表的最上方. 因为咱们的`main`函数基本是最先调用的函数了,所以排在了倒数第二位,还有一个关注点,排在最顶部最后一个调用的函数是`rust_begin_unwind`,该函数的目的就是进行栈展开,呈现这些列表信息给我们。 +上面的代码就是一次栈展开(也称栈回溯),它包含了函数调用的顺序,当然按照逆序排列:最近调用的函数排在列表的最上方。因为咱们的 `main` 函数基本是最先调用的函数了,所以排在了倒数第二位,还有一个关注点,排在最顶部最后一个调用的函数是 `rust_begin_unwind`,该函数的目的就是进行栈展开,呈现这些列表信息给我们。 -要获取到栈回溯信息,你还需要开启`debug`标志,该标志在使用`cargo run`或者`cargo build`时自动开启(这两个方式是`Debug`运行方式). 同时,栈展开信息在不同操作系统或者Rust版本上也所有不同。 +要获取到栈回溯信息,你还需要开启 `debug` 标志,该标志在使用 `cargo run` 或者 `cargo build` 时自动开启(这两个操作默认是 `Debug` 运行方式). 同时,栈展开信息在不同操作系统或者 Rust 版本上也所有不同。 ## panic时的两种终止方式 -当出现`panic!`时,程序提供了两种方式来处理终止流程: **栈展开** 和 **直接终止**. +当出现 `panic!` 时,程序提供了两种方式来处理终止流程: **栈展开** 和 **直接终止**。 -其中,默认的方式就是`栈展开`,这意味着Rust会回溯栈上数据和函数调用,因此也意味着更多的善后工作,好处是给于充分的报错信息和栈调用信息,便于事后的问题复盘。`直接终止`,顾名思义,不清理数据就直接推出程序,善后工作交与操作系统来负责。 +其中,默认的方式就是 `栈展开`,这意味着 Rust 会回溯栈上数据和函数调用,因此也意味着更多的善后工作,好处是可以给出充分的报错信息和栈调用信息,便于事后的问题复盘。`直接终止`,顾名思义,不清理数据就直接退出程序,善后工作交与操作系统来负责。 -对于绝大多数用户,使用默认选择是最好的,但是当你关心最终编译出的二进制可执行文件大小时,那么可以尝试去使用直接终止的方式,例如下面的配置修改`Cargo.toml`文件,实现在[`release`](../first-try/cargo.md#手动编译和运行项目)模式下遇到`panic`直接终止: +对于绝大多数用户,使用默认选择是最好的,但是当你关心最终编译出的二进制可执行文件大小时,那么可以尝试去使用直接终止的方式,例如下面的配置修改 `Cargo.toml` 文件,实现在[`release`](../first-try/cargo.md#手动编译和运行项目)模式下遇到 `panic` 直接终止: ```rust [profile.release] panic = 'abort' ``` ## 线程`panic`后,程序会否终止? -长话短说,如果是`main`线程,则程序会终止,如果是其它子线程,该线程会终止。因此,尽量不要在`main`线程中做太多任务,将这些任务交由子线程去做,就算`panic`也不会导致整个程序的结束。 +长话短说,如果是 `main` 线程,则程序会终止,如果是其它子线程,该线程会终止,但是不会影响 `main` 线程。因此,尽量不要在 `main` 线程中做太多任务,将这些任务交由子线程去做,就算子线程 `panic` 也不会导致整个程序的结束。 具体解析见[panic原理剖析](#panic原理剖析) ## 何时该使用panic! -下面让我们大概罗列下合适适合使用`panic`,虽然原则上,你理解了之前的内容后,会自己作出合适的选择,但是罗列出来可以帮助你强化这一点。 +下面让我们大概罗列下何时适合使用 `panic`,也许经过之前的学习,你已经能够对 `panic` 的使用有了自己的看法,但是我们还是会罗列一些常见的用法来加深你的理解。 -先来一点背景知识,在前面章节我们粗略讲过`Result`这个枚举类型,它是用来表示函数的返回结果: +先来一点背景知识,在前面章节我们粗略讲过 `Result` 这个枚举类型,它是用来表示函数的返回结果: ```rust enum Result { Ok(T), Err(E), } ``` -当没有错误发生时,函数返回一个用`Result`类型包裹的值`Ok(T)`,当错误时,返回一个`Err(E)`。对于`Result`返回我们有很多处理方法,最简单粗暴的就是`unwrap`和`expect`,这两个函数非常类似,我们以`unwrap`举例: +当没有错误发生时,函数返回一个用 `Result` 类型包裹的值 `Ok(T)`,当错误时,返回一个 `Err(E)`。对于 `Result` 返回我们有很多处理方法,最简单粗暴的就是 `unwrap` 和 `expect`,这两个函数非常类似,我们以 `unwrap` 举例: ```rust 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` 简而言之:成功则返回值,失败则 `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(); ``` -例如上面的例子,`"127.0.0.1"`就是`ip`地址,因此我们知道`parse`方法一定会成功,那么就可以直接用`unwrap`方法进行处理。 +例如上面的例子,`"127.0.0.1"` 就是 `ip` 地址,因此我们知道 `parse` 方法一定会成功,那么就可以直接用 `unwrap` 方法进行处理。 -当然,如果该字符串是来自于用户输入,那在实际项目中,就必须用错误处理的方式,而不是`unwrap`,否则你的程序一天要崩溃几十万次吧! +当然,如果该字符串是来自于用户输入,那在实际项目中,就必须用错误处理的方式,而不是 `unwrap`,否则你的程序一天要崩溃几十万次吧! #### 可能导致全局有害状态时 有害状态大概分为几类: - 非预期的错误 -- 之后代码的运行会受到明显可见的影响 +- 后续代码的运行会受到显著影响 - 内存安全的问题 当错误预期会出现时,返回一个错误较为合适,例如解析器接收到格式错误的数据,HTTP请求接收到错误的参数甚至该请求内的任何错误(不会导致整个程序有问题,只影响该此请求)。 **因为错误是可预期的,因此也是可以处理的**。 -当启动时某个流程发生了错误,导致了后续代码的运行造成影响,那么就应该使用`panic`,而不是处理错误后,继续运行,当然你可以通过重试的方式来继续。 +当启动时某个流程发生了错误,对后续代码的运行造成了影响,那么就应该使用 `panic`,而不是处理错误后继续运行,当然你可以通过重试的方式来继续。 -上面提到过,数组访问越界,就要`panic`的原因,这个就是属于内存安全的范畴,一旦内存访问不安全,那么我们无法保证自己的程序会怎么运行下去,也无法保证逻辑和数据的正确性。 +上面提到过,数组访问越界,就要 `panic` 的原因,这个就是属于内存安全的范畴,一旦内存访问不安全,那么我们就无法保证自己的程序会怎么运行下去,也无法保证逻辑和数据的正确性。 ## panic原理剖析 本来不想写这块儿内容,因为真的难写,但是转念一想,既然号称圣经,那么本书就得与众不同,避重就轻显然不是该有的态度。 -当调用`panic!`宏时,它会 -1. 格式化`panic`信息,然后使用该信息作为参数,调用`std::panic::panic_any()`函数 -2. `panic_any`会检查应用是否使用了`panic hook`,如果使用了,该`hook`函数会被调用 -3. 当`hook`函数返回后,当前的线程就开始进行栈展开:从`panic_any`开始,如果寄存器或者栈因为某些原因信息错乱了,那很可能该展开会发生异常,最终线程会直接停止,展开也无法继续进行。 -4. 展开的过程是一帧一帧的去回溯整个栈,每个帧的数据都会随之被丢弃,但是在展开过程中,你可能会遇到被用户标记为`catching`的帧(通过`std::panic::catch_unwind()`函数标记),此时用户提供的`catch`函数会被调用,展开也随之停止: 当然,如果`catch`选择在内部调用`std::panic::resume_unwind()`函数,则展开还会继续。 +当调用 `panic!` 宏时,它会 +1. 格式化 `panic` 信息,然后使用该信息作为参数,调用 `std::panic::panic_any()` 函数 +2. `panic_any` 会检查应用是否使用了 `panic hook`,如果使用了,该 `hook` 函数就会被调用(hook是一个钩子函数,是外部代码设置的,用于在panic触发时,执行外部代码所需的功能) +3. 当 `hook` 函数返回后,当前的线程就开始进行栈展开:从 `panic_any` 开始,如果寄存器或者栈因为某些原因信息错乱了,那很可能该展开会发生异常,最终线程会直接停止,展开也无法继续进行 +4. 展开的过程是一帧一帧的去回溯整个栈,每个帧的数据都会随之被丢弃,但是在展开过程中,你可能会遇到被用户标记为 `catching` 的帧(通过 `std::panic::catch_unwind()` 函数标记),此时用户提供的 `catch` 函数会被调用,展开也随之停止:当然,如果 `catch` 选择在内部调用 `std::panic::resume_unwind()` 函数,则展开还会继续。 -还有一种情况,在展开过程中,如果展开本身`panic`了,那展开线程会终止,展开也随之停止。 +还有一种情况,在展开过程中,如果展开本身 `panic` 了,那展开线程会终止,展开也随之停止。 -一旦线程展开被终止或者完成,最终的输出结果是取决于哪个线程`panic`:对于`main`线程,操作系统提供的终止功能`core::intrinsics::abort()`会被调用,最终结束当前的`panic`进程;如果是其它子线程,那么线程就会简单的终止,同时信息会在稍后通过`std::thread::join()`进行收集. +一旦线程展开被终止或者完成,最终的输出结果是取决于哪个线程 `panic:对于 `main` 线程,操作系统提供的终止功能 `core::intrinsics::abort()` 会被调用,最终结束当前的 `panic` 进程;如果是其它子线程,那么子线程就会简单的终止,同时信息会在稍后通过 `std::thread::join()` 进行收集。 diff --git a/book/contents/basic/trait/advance-trait.md b/book/contents/basic/trait/advance-trait.md index b5fe66d9..41a1aa1b 100644 --- a/book/contents/basic/trait/advance-trait.md +++ b/book/contents/basic/trait/advance-trait.md @@ -1,6 +1,6 @@ # 深入了解特征 -特征之于Rust更甚于接口之于其他语言,因此特征在Rust中很重要也相对较为复杂,我们决定把特征分为两篇进行介绍,[第一篇](./trait.md)在之前已经讲过,现在就是第二篇:关于特征的进阶篇,会讲述一些你不常用到但是该了解的特性。 +特征之于Rust更甚于接口之于其他语言,因此特征在Rust中很重要也相对较为复杂,我们决定把特征分为两篇进行介绍,[第一篇](./trait.md)在之前已经讲过,现在就是第二篇:关于特征的进阶篇,会讲述一些不常用到但是你该了解的特性。 ## 关联类型 在方法一章中,我们将到了[关联函数](../method.md#关联函数),但是实际上关联类型和关联函数并没有任何交集,虽然它们的名字有一半的交集。 @@ -16,7 +16,7 @@ pub trait Iterator { 以上是标准库中的迭代器特征 `Iterator`,它有一个 `Item` 关联类型,用于替代遍历的值的类型。 -同时, `next` 方法也返回了一个 `Item` 类型,不过使用 `Option` 枚举进行了包裹,假如迭代器中的值是 `i32` 类型,那么调用 `next` 方法就将获取一个 `Option` 的值。 +同时,`next` 方法也返回了一个 `Item` 类型,不过使用 `Option` 枚举进行了包裹,假如迭代器中的值是 `i32` 类型,那么调用 `next` 方法就将获取一个 `Option` 的值。 还记得 `Self` 吧?在之前的章节[提到过](./trait-object#Self与self), `Self` 用来指代当前的特征实例,那么 `Self::Item` 就用来指代特征实例中具体的 `Item` 类型: ```rust @@ -29,14 +29,14 @@ impl Iterator for Counter { 在上述代码中,我们为 `Counter` 类型实现了 `Iterator` 特征,那么 `Self` 就是当前的 `Iterator` 特征对象, `Item` 就是 `u32` 类型。 -聪明的读者之所以聪明,因为你们喜欢联想和举一反三,同时你们也喜欢提问:为何不用泛型,例如如下代码 +聪明的读者之所以聪明,是因为你们喜欢联想和举一反三,同时你们也喜欢提问:为何不用泛型,例如如下代码 ```rust pub trait Iterator { fn next(&mut self) -> Option; } ``` -答案其实很简单,为了代码的可读性. 当你使用了泛型后,你需要在所有地方都这样写 `Iterator`,而使用了关联类型,你只需要这样写 `Iterator`,当类型定义复杂时,这种写法可以极大的增加可读性: +答案其实很简单,为了代码的可读性,当你使用了泛型后,你需要在所有地方都这样写 `Iterator`,而使用了关联类型,你只需要这样写 `Iterator`,当类型定义复杂时,这种写法可以极大的增加可读性: ```rust pub trait CacheableItem: Clone + Default + fmt::Debug + Decodable + Encodable { type Address: AsRef<[u8]> + Clone + fmt::Debug + Eq + Hash; @@ -44,7 +44,7 @@ pub trait CacheableItem: Clone + Default + fmt::Debug + Decodable + Encodable { } ``` -例如上面的代码, `Address` 自然远比 `AsRef<[u8]> + Clone + fmt::Debug + Eq + Hash` 的要简单的多,而且含义清晰。 +例如上面的代码, `Address` 的写法自然远比 `AsRef<[u8]> + Clone + fmt::Debug + Eq + Hash` 要简单的多,而且含义清晰。 再例如,如果使用泛型,你将得到以下的代码: ```rust @@ -57,7 +57,7 @@ fn difference(container: &C) -> i32 C : Container {...} ``` -而使用关联类型,将得到可读性好的多的代码: +可以看到,由于使用了泛型,导致函数头部也必须增加泛型的声明,而使用关联类型,将得到可读性好的多的代码: ```rust trait Container{ type A; @@ -108,7 +108,7 @@ fn main() { ``` 上面的代码主要干了一件事,就是为 `Point` 结构体提供 `+` 的能力,这就是**运算符重载**,不过Rust并不支持创建自定义运算符,你也无法为所有运算符进行重载,目前来说,只有定义在 `std::ops` 中的运算符才能进行重载。 -跟 `+` 对应的特征是 `std::ops::Add`,我们在之前也看过它的定义 `trait Add`,但是上面的例子中并没有为 `Point` 实现 `Add` 特征,而是实现了 `Add` 特征,这意味着我们使用了 `RHS` 的默认类型,也就是 `Self`。换句话说,我们这里定义的是两个相同的 `Point` 类型相加,因此无需指定 `RHS`。 +跟 `+` 对应的特征是 `std::ops::Add`,我们在之前也看过它的定义 `trait Add`,但是上面的例子中并没有为 `Point` 实现 `Add` 特征,而是实现了 `Add` 特征(没有默认泛型类型参数),这意味着我们使用了 `RHS` 的默认类型,也就是 `Self`。换句话说,我们这里定义的是两个相同的 `Point` 类型相加,因此无需指定 `RHS`。 与上面的例子相反,下面的例子,我们来创建两个不同类型的相加: ```rust @@ -126,7 +126,7 @@ impl Add for Millimeters { } ``` -这里,是进行 `Millimeters + Meters` 的操作,因此此时不能再使用默认的 `RHS`,否则就会变成 `Millimeters + Millimeters` 的形式。使用 `Add` 可以将 `RHS` 指定为 `Meters`,那么 `fn add(self, rhs: RHS)` 自然而言的变成了 `Millimeters` 和 `Meters` 的相加。 +这里,是进行 `Millimeters + Meters` 两种数据类型的 `+` 操作,因此此时不能再使用默认的 `RHS`,否则就会变成 `Millimeters + Millimeters` 的形式。使用 `Add` 可以将 `RHS` 指定为 `Meters`,那么 `fn add(self, rhs: RHS)` 自然而言的变成了 `Millimeters` 和 `Meters` 的相加。 默认类型参数主要用于两个方面: 1. 减少实现的样板代码 @@ -134,12 +134,12 @@ impl Add for Millimeters { 之前的例子就是第一点,虽然效果也就那样。在 `+` 左右两边都是同样类型时,只需要 `impl Add` 即可,否则你需要 `impl Add`,嗯,会多写几个字:) -对于第二点,也很好理解,如果你在一个复杂类型的基础上,新引入一个泛型参数,可能需要修改很多地方,但是如果新引入的泛型参数有了默认类型,情况就会好很多。 +对于第二点,也很好理解,如果你在一个复杂类型的基础上,新引入一个泛型参数,可能需要修改很多地方,但是如果新引入的泛型参数有了默认类型,情况就会好很多,添加泛型参数后,使用这个类型的代码需要逐个在类型提示部分添加泛型参数,就很麻烦;但是有了默认参数(且默认参数取之前的实现里假设的值的情况下)之后,原有的使用这个类型的代码就不需要做改动了。 归根到底,默认泛型参数,是有用的,但是大多数情况下,咱们确实用不到,当需要用到时,大家再回头来查阅本章即可,**手上有剑,心中不慌**。 ## 调用同名的方法 -不同特征拥有同名的方法是很正常的事情,你没有任何办法阻止这一点,甚至除了特征上的同名方法外,在你的类型上,也有同名方法: +不同特征拥有同名的方法是很正常的事情,你没有任何办法阻止这一点;甚至除了特征上的同名方法外,在你的类型上,也有同名方法: ```rust trait Pilot { fn fly(&self); @@ -227,13 +227,13 @@ impl Animal for Dog { } fn main() { - println!("A baby dog is called a {}",Dog::baby_name()); + println!("A baby dog is called a {}", Dog::baby_name()); } ``` -就像人类妈妈会给自己的宝宝起爱称一样,狗狗妈妈也会。狗狗妈称呼自己的宝宝为**Spot**,其它动物称呼狗宝宝为**puppy**,这个时候假如有其它动物,不知道该称如何呼狗宝宝,它需要查询一下。 +就像人类妈妈会给自己的宝宝起爱称一样,狗妈妈也会。狗妈妈称呼自己的宝宝为**Spot**,其它动物称呼狗宝宝为**puppy**,这个时候假如有动物不知道该称如何呼狗宝宝,它需要查询一下。 -但是 `Dog::baby_name()` 的调用方式显然不行,这是狗妈妈对宝宝的爱称,但是如果你试图这样查询: +`Dog::baby_name()` 的调用方式显然不行,因为这只是狗妈妈对宝宝的爱称,可能你会想到通过下面的方式查询其他动物对狗狗的称呼: ```rust fn main() { println!("A baby dog is called a {}", Animal::baby_name()); @@ -251,7 +251,7 @@ error[E0283]: type annotations needed // 需要类型注释 = note: cannot satisfy `_: Animal` ``` -因为单纯从 `Animal::baby_name()` 上,编译器无法得到任何有效的信息:你想获取哪个动物宝宝的名称?狗宝宝?猪宝宝?还是熊宝宝? +因为单纯从 `Animal::baby_name()` 上,编译器无法得到任何有效的信息:实现 `Animal` 特征的类型可能有很多,你究竟是想获取哪个动物宝宝的名称?狗宝宝?猪宝宝?还是熊宝宝? 此时,就需要使用**完全限定语法**。 @@ -269,7 +269,10 @@ fn main() { ::function(receiver_if_method, next_arg, ...); ``` -对于关联函数,其没有一个方法接收器( `receiver` ),故只会有其他参数的列表。**可以选择在任何函数或方法调用处使用完全限定语法**,同时,你还能省略任何Rust能够从程序中的其他信息中推导出的的部分。只有当存在多个同名实现而 Rust 需要帮助以便知道我们希望调用哪个实现时,才需要使用这个较为冗长的语法。 +上面定义中,第一个参数是方法接收器`receiver`(三种`self`),只有方法才拥有,例如关联函数就没有`receiver`。 + +完全限定语法可以用于任何函数或方法调用,那么我们为何很少用到这个语法?原因是Rust编译器能根据上下文自动推导出调用的路径,因此大多数时候,我们都无需使用完全限定语法。只有当存在多个同名函数或方法,且Rust无法区分出你想调用的目标函数时,该用法才能真正有用武之地。 + ## 特征定义中的特征约束 @@ -332,13 +335,13 @@ impl fmt::Display for Point { 在[特征](./trait#特征定义与实现的位置(孤儿规则))章节中,有提到孤儿规则,简单来说,就是特征或者类型必需至少有一个是本地的,才能在此类型上定义特征。 - 这里提供一个办法来绕过孤儿规则,那就是使用**newtype模式**,简而言之: 就是为一个[元组结构体](../compound-type/struct.md#元组结构体)创建新类型。该元组结构体封装有一个字段,该字段就是希望实现特征的具体类型。 +这里提供一个办法来绕过孤儿规则,那就是使用**newtype模式**,简而言之:就是为一个[元组结构体](../compound-type/struct.md#元组结构体)创建新类型。该元组结构体封装有一个字段,该字段就是希望实现特征的具体类型。 - 该封装类型是本地的,因此我们可以为此类型实现外部的特征。 +该封装类型是本地的,因此我们可以为此类型实现外部的特征。 - `newtype` 不仅仅能实现以上的功能,而且它在运行时没有任何性能损耗,因为在编译期,该类型会被自动忽略。 +`newtype` 不仅仅能实现以上的功能,而且它在运行时没有任何性能损耗,因为在编译期,该类型会被自动忽略。 - 下面来看一个例子,我们有一个动态数组类型: `Vec`,它定义在标准库中,还有一个特征 `Display`,它也定义在标准库中,如果没有 `newtype`,我们是无法为 `Vec` 实现 `Display` 的: +下面来看一个例子,我们有一个动态数组类型: `Vec`,它定义在标准库中,还有一个特征 `Display`,它也定义在标准库中,如果没有 `newtype`,我们是无法为 `Vec` 实现 `Display` 的: ```console @@ -377,7 +380,7 @@ fn main() { 既然`new type`有这么多好处,它有没有不好的地方呢?答案是肯定的。注意到我们怎么访问里面的数组吗?`self.0.join(", ")`,是的,很啰嗦,因为需要先从 `Wrapper` 中取出数组: `self.0`,然后才能执行 `join` 方法。 -类似的,任何数组上的方法,你都无法直接调用,需要先用 `self.0`取出数组,然后再进行调用。 +类似的,任何数组上的方法,你都无法直接调用,需要先用 `self.0` 取出数组,然后再进行调用。 当然,解决办法还是有的,要不怎么说Rust是极其强大灵活的编程语言!Rust提供了一个特征叫[ `Deref` ](../../traits/deref.md),实现该特征后,可以自动做一层类似类型转换的操作,可以将 `Wrapper` 变成 `Vec` 来使用。这样就会像直接使用数组那样去使用 `Wrapper`,而无需为每一个操作都添加上 `self.0`。 diff --git a/book/contents/basic/trait/trait-object.md b/book/contents/basic/trait/trait-object.md index f9dbaa62..fdfc504d 100644 --- a/book/contents/basic/trait/trait-object.md +++ b/book/contents/basic/trait/trait-object.md @@ -231,7 +231,7 @@ fn main() { #### &dyn和Box\的区别 前文提到, `&dyn` 和 `Box` 都可以用于特征对象,因此在功能上 `&dyn` 和 `Box` 几乎没有区别,唯一的区别就是:`&dyn` 减少了一次指针调用。 -因为 `Box` 是一个宽指针(`fat pointer`),它内部保存一个指针指向 `vtable`,然后通过 `vtable` 查询到具体的函数指针,最后进行调用。 +因为 `Box` 是一个宽指针(`fat pointer`),它需要一次额外的解引用后,才能获取到指向`vtable`的指针,然后再通过该指针访问 `vtable` 查询到具体的函数指针,最后进行调用。 所以,如果你在乎性能,又想使用特征对象简化代码,可以优先考虑 `&dyn`。 diff --git a/book/contents/basic/variable.md b/book/contents/basic/variable.md index 8d89cc15..e040dfb2 100644 --- a/book/contents/basic/variable.md +++ b/book/contents/basic/variable.md @@ -175,14 +175,14 @@ let spaces = " "; let spaces = spaces.len(); ``` -这种结构是允许的,因为第一个 `spaces` 变量是一个字符串类型,第二个 `spaces` 变量是一个全新的变量且和第一个具有相同的变量名,且是一个数值类型。所以变量遮蔽可以帮我们节省些脑细胞,不用去想如`spaces_str` 和 `spaces_num`此类的变量名;相反我们可以重复使用更简单的 `spaces` 变量名。 你也可以不用`let`: +这种结构是允许的,因为第一个 `spaces` 变量是一个字符串类型,第二个 `spaces` 变量是一个全新的变量且和第一个具有相同的变量名,且是一个数值类型。所以变量遮蔽可以帮我们节省些脑细胞,不用去想如`spaces_str` 和 `spaces_num`此类的变量名;相反我们可以重复使用更简单的 `spaces` 变量名。 如果你不用`let`: ```rust, let mut spaces = " "; spaces = spaces.len(); ``` -运行一下 +运行一下,你就会发现编译器报错: ```console $ cargo run diff --git a/book/contents/errors/panic-codes.md b/book/contents/errors/panic-codes.md index 3f585cf7..1e3e8c21 100644 --- a/book/contents/errors/panic-codes.md +++ b/book/contents/errors/panic-codes.md @@ -7,5 +7,5 @@ String slice range indices must occur at valid UTF-8 character boundaries. If yo > > 比方说有一个 `u8` ,它可以存放从 0 到 255 的值。那么当你将其修改为范围之外的值,比如 256,则会发生**整型溢出**。关于这一行为 Rust 有一些有趣的规则。当在 debug 模式编译时,Rust 会检查整型溢出若存在这些问题则使程序在编译时 *panic*。Rust 使用这个术语来表明程序因错误而退出。 [该章节](../../errors/panic.md)会详细介绍 panic。 > -> 在当使用 `--release` 参数进行 release 模式构建时,Rust **不**检测溢出。相反当检测到整型溢出时,Rust 会进行一种被称为二进制补码的方式进行(*two’s complement wrapping*)操作。简而言之,大于该类型最大值的数值会被补码转换成该类型能够支持的对应数字的最小值。比如在 `u8` 的情况下,256 变成 0,257 变成 1,依此类推。程序不会 panic,但是该变量的值可能不是你期望的值。依赖整型溢出包裹的行为不是一种正确的做法。 +> 在当使用 `--release` 参数进行 release 模式构建时,Rust **不**检测溢出。相反,当检测到整型溢出时,Rust 会按照补码循环溢出(*two’s complement wrapping*)的规则处理。简而言之,大于该类型最大值的数值会被补码转换成该类型能够支持的对应数字的最小值。比如在 `u8` 的情况下,256 变成 0,257 变成 1,依此类推。程序不会 panic,但是该变量的值可能不是你期望的值。依赖这种默认行为的代码都应该被认为是错误的代码。 > \ No newline at end of file diff --git a/book/contents/first-try/cargo.md b/book/contents/first-try/cargo.md index 2f171a97..e051529a 100644 --- a/book/contents/first-try/cargo.md +++ b/book/contents/first-try/cargo.md @@ -97,7 +97,7 @@ $ cargo check Finished dev [unoptimized + debuginfo] target(s) in 0.06s ``` -> Rust虽然编译速度还行,但是还是不能Go语言相提并论,因为Rust需要做很多复杂的编译优化和语言特性解析, 甚至连如何优化编译速度都成了一门学问[优化编译速度](../compiler/spped-up.md) +> Rust虽然编译速度还行,但是还是不能Go语言相提并论,因为Rust需要做很多复杂的编译优化和语言特性解析, 甚至连如何优化编译速度都成了一门学问[优化编译速度](../compiler/speed-up.md) ## cargo.toml 和 cargo.lock diff --git a/book/contents/into-rust.md b/book/contents/into-rust.md index 81d82236..dc7e79ed 100644 --- a/book/contents/into-rust.md +++ b/book/contents/into-rust.md @@ -1,135 +1,156 @@ -# 进入Rust编程世界 +# 进入 Rust 编程世界 -## Rust发展历程 +## Rust 发展历程 -Rust 最早是 Mozilla 雇员 Graydon Hoare 的个人项目,从 2009 年开始,得到了 Mozilla 研究院的支助,2010 年项目对外公布。2010 ~2011 年间实现自举。从此以后,Rust在设计变化->崩溃的边缘反复横跳(历程极其艰辛),终于在 2015 年 5 月 15日发布1.0 版。在此研发过程中,Rust 建立了一个强大且活跃社区,形成了一整套完善稳定的项目贡献机制(Rust能够飞速发展,与这一点密不可分)。Rust 现在由 [Rust项目开发者社区](https://github.com/rust-lang/rust)维护。 +Rust 最早是 Mozilla 雇员 Graydon Hoare 的个人项目。从 2009 年开始,得到了 Mozilla 研究院的资助。2010 年项目对外公布,2010 ~2011 年间实现自举。从此以后,Rust 在设计变化->崩溃的边缘反复横跳(历程极其艰辛)。终于,在 2015 年 5 月 15 日发布 1.0 版。 -大家可能疑惑Rust为啥用了这么久才到1.0版本?与之相比,Go语言2009年发布,却在2012年仅用3年就发布了1.0版本。首先是因为Rust语言特性较为复杂,因此需要全盘考虑的问题非常多;其次,Rust当时的参与者太多,七嘴八舌的声音很多,导致了众口难调,而Rust开发团队又非常重视社区的意见 ;最后,一旦1.0快速发布,那么后续大部分语言特性就无法再修改,对于有完美强迫症的Rust开发者团队来说,某种程度上的不完美是不可接受的。因此,Rust语言用了足足6年时间,才发布了尽善尽美的1.0版本。 +在此研发过程中,Rust 建立了一个强大且活跃社区,形成了一整套完善稳定的项目贡献机制(Rust 能够飞速发展,与这一点密不可分)。Rust 现在由 [Rust 项目开发者社区](https://github.com/rust-lang/rust) 维护。 + +大家可能疑惑 Rust 为啥用了这么久才到 1.0 版本?与之相比,Go 语言 2009 年发布,却在 2012 年仅用 3 年就发布了 1.0 版本。 + +- 首先,因为 Rust 语言特性较为复杂,所以需要全盘考虑的问题非常多; +- 其次,Rust 当时的参与者太多,七嘴八舌的声音很多,众口难调,而 Rust 开发团队又非常重视社区的意见; +- 最后,一旦 1.0 快速发布,那么后续大部分语言特性就无法再修改,对于有完美强迫症的 Rust 开发者团队来说,某种程度上的不完美是不可接受的。 + +因此,Rust 语言用了足足 6 年时间,才发布了尽善尽美的 1.0 版本。 ## 为何又来了一门新语言? -为何需要一个新语言?简而言之,因为还缺一门无GC、**性能高**、**工程性强**、语言级安全性以及能同时得到工程派和学院派认可的语言,而Rust算是这样的语言。你也可以回忆下熟悉的语言,看是不是有另外一门语言可以同时满足这些需求: ) -而Rust最为人诟病的点,也就一个:学习曲线陡峭,其实严格来说,当语言生态起来后,这个不算问题。 +简而言之,因为还缺一门无 GC、性能高、工程性强、语言级安全性以及能同时得到工程派和学院派认可的语言,而 Rust 算是这样的语言。你也可以回忆下熟悉的语言,看是不是有另外一门语言可以同时满足这些需求:) + +Rust 最为人诟病的点,也就一个:学习曲线陡峭。不过其实严格来说,当语言生态起来后,这个不算问题。 ### 缓解内卷 -说Rust作为新语言会增加内卷,其实恰恰相反,Rust可以缓解内卷。为何不说C++内卷说Java、Python、JS内卷?不就是后几个相对简单、上手容易嘛?而Rust怎么看也是C++级别的上手难度O,O -其实从我内心不可告人的角度出发,并不希望Rust大众化,因为这样可以保饭碗、保薪资,还能更有行业内地位。但是从对Rust的喜爱角度出发,我还是希望能卷一些,但是。。。目前来看真的卷不动,现在全世界范围Rust的需求都大于供给,特别是优秀的Rust程序员更是难寻。 +有人说 Rust 作为新语言会增加内卷,其实恰恰相反,Rust 可以缓解内卷。为何不说 C++内卷,说 Java、Python、JS 内卷?不就是后几个相对简单、上手容易嘛?而 Rust 怎么看也是 C++ 级别的上手难度。 -与Go语言相比,一个优秀的Rust程序员所需的门槛高得多,例如融汇贯通Rust语言各种中高级特性、闭着眼睛躺过各种坑、不用回忆无需查找就能立刻写出最合适的包/模块/方法、性能/安全/工程性的权衡选择信手拈来、深层性能优化易如反掌、异步编程小菜一碟,更别说Rust之外的操作系统、网络、算法等等相关知识。 +其实从我内心不可告人的角度出发,并不希望 Rust 大众化,因为这样可以保饭碗、保薪资,还能更有行业内地位。但是从对 Rust 的喜爱角度出发,我还是希望能卷一些。但是,目前来看真的卷不动,现在全世界范围内 Rust 的需求都大于供给,特别是优秀的 Rust 程序员更是难寻。 -所以,Rust可以缓解内卷,而不是增加内卷,可以说是程序员的福音,不再是被随意替换的螺丝钉。 +与 Go 语言相比,成为一个优秀的 Rust 程序员所需的门槛高得多,例如融汇贯通 Rust 语言各种中高级特性、闭着眼睛躺过各种坑、不用回忆无需查找就能立刻写出最合适的包/模块/方法、性能/安全/工程性的权衡选择信手拈来、深层性能优化易如反掌、异步编程小菜一碟,更别说 Rust 之外的操作系统、网络、算法等等相关知识。 + +所以,Rust 可以缓解内卷,而不是增加内卷。可以说是程序员的福音,不再是被随意替换的螺丝钉。 ### 效率 -下面从三个角度来谈谈Rust的效率:学习、运行、开发。 + +下面从三个角度来谈谈 Rust 的效率:学习、运行、开发。 #### 学习效率 -众所周知,Rust学习曲线陡峭,最初我对此说法还嗤之以鼻,随着不断的深入,我现在也很认可这个说法。Rust之难,不在于语言特性,这些都可以很容易学到,而在于: + +众所周知,Rust 学习曲线陡峭。最初我对此说法还嗤之以鼻,随着不断的深入,我现在也很认可这个说法。Rust 之难,不在于语言特性,这些都可以很容易学到,而在于: - 实践中如何融会贯通的运用 -- 遇到了坑时(生命周期、借用错误,自引用等)如何迅速、正确的解决 +- 遇到了坑时(生命周期、借用错误,自引用等)如何迅速、正确的解决 - 大量的标准库方法记忆及熟练使用,这些是保证开发效率的关键 -- 心智负担较重,特别是初中级阶段时 +- 心智负担较重,特别是初中级阶段 -好在,针对这些,目前国内有了一门非常全面的[Rust学习教程](https://github.com/sunface/rust-course)(非官方那本书),可以有效降低Rust的学习和使用门槛。 +好在,针对这些,目前国内有了一门非常全面的 [Rust 学习教程](https://github.com/sunface/rust-course)(非官方那本书),可以有效降低 Rust 的学习和使用门槛。 #### 运行效率 -得益于各种零开销抽象、深入到底层的优化潜力、优质的标准库和三方库实现,Rust具备非常优秀的性能,和C语言、C++是[一个级别](https://benchmarksgame-team.pages.debian.net/benchmarksgame/fastest/rust.html)。 -同时Rust有一个极大的好处:只要按照正确的方式使用Rust,无需性能优化,就能实现非常优秀的表现,不可谓不惊艳, +得益于各种零开销抽象、深入到底层的优化潜力、优质的标准库和三方库实现,Rust 具备非常优秀的性能,和 C、C++ 是 [一个级别](https://benchmarksgame-team.pages.debian.net/benchmarksgame/fastest/rust.html)。 + +同时 Rust 有一个极大的好处:只要按照正确的方式使用 Rust,无需性能优化,就能实现非常优秀的表现,不可谓不惊艳, -现在有不少用Rust重写的工具、平台都超过了原来用C、C++实现的版本,将老前辈拍死在沙滩上,俨然成为一种潮流~~ +现在有不少用 Rust 重写的工具、平台都超过了原来用 C、C++ 实现的版本,将老前辈拍死在沙滩上,俨然成为一种潮流~~ #### 开发效率 -Rust的开发效率可以先抑后扬来形容,在刚开始上手写项目时,你的开发速度将显著慢于go、java等语言,一旦开始熟悉标准库常用的方法,开发效率将大幅提升,甚至当形成肌肉记忆后,开发效率将不会慢于这些语言,而且原生就能写出高质量、安全、高效的代码,可以说中高级级Rust程序员就是高效程序员的代名词。 + +Rust 的开发效率可以先抑后扬来形容。在刚开始上手写项目时,你的开发速度将显著慢于 go、java 等语言,不过,一旦开始熟悉标准库常用的方法,开发效率将大幅提升,甚至当形成肌肉记忆后,开发效率将不会慢于这些语言,而且原生就能写出高质量、安全、高效的代码,可以说中高级 Rust 程序员就是高效程序员的代名词。 ### 个人的好处 -学习Rust对个人也有极大的好处。 + +学习 Rust 对个人也有极大的好处。 #### 成为更好的程序员 -要学好Rust,你需要深入理解内存、堆栈、引用、变量作用域这些其它高级语言往往不会深入接触的内容,同时Rust会通过语法、编译器和clippy这些静态检查工具半帮助半强迫的让你成为更优秀的程序员,写出更好的代码。 -同时,当你掌握了Rust,你自发性的就会想要去做一些更偏底层的事情,这些都可以帮助你更加了解操作系统、网络、性能优化等底层知识,也会间接或者直接的接触到各种算法、数据结构实现。 +要学好 Rust,你需要深入理解内存、堆栈、引用、变量作用域等这些其它高级语言往往不会深入接触的内容。另外,Rust 会通过语法、编译器和 clippy 这些静态检查工具半帮助半强迫的让你成为更优秀的程序员,写出更好的代码。 + +同时,当你掌握了 Rust,你就会自发性的想要去做一些更偏底层的事情,这些都可以帮助你更加了解操作系统、网络、性能优化等底层知识,也会间接或者直接的接触到各种算法和数据结构的实现。 慢慢的,你就在成为那个更好的程序员,也是更优秀的自己。 #### 增加不可替代性 -语言难学,也有好处,一旦掌握,你将具备较强的不可替代性,不再是一个简单的工具人角色。看看现在内卷严重的Java,工具人有多少!一个人离职,另外一个人很快就能替补上。 -当然,我不是说Rust会给公司带来这种隐形的维护成本,毕竟这其实是一种双赢,公司收获了更优秀的程序员(不可否认的是Rust程序员普遍确实水平更高,毕竟都是有很好的其它语言基础、也很有追求的自驱性人才),而你也收获了更稳定的工作环境甚至是更高的收入。 +语言难学,也有好处。一旦掌握,你将具备较强的不可替代性,不再是一个简单的工具人角色。看看现在内卷严重的 Java,工具人有多少!一个人离职,另外一个人很快就能替补上。 + +当然,我不是说 Rust 会给公司带来这种隐形的维护成本,毕竟这其实是一种双赢,公司收获了更优秀的程序员(不可否认的是 Rust 程序员普遍确实水平更高,毕竟都是有很好的其它语言基础、也很有追求的自驱型人才),而你也收获了更稳定的工作环境,甚至是更高的收入。 ### 团队的好处 -先不说安全、可靠性等对公司团队非常友好的特性,就说Rust程序只要能跑起来,那代码质量其实就是相当不错的,因为Rust编译器、clippy啥的实在是严师厉友,甚至有些鸡毛。 -正因为这较高的质量下限,我们在review时并不用担心潜在的各种坑,因此可以实现快速的开发、review、merge流程。 +先不说安全、可靠性等对公司团队非常友好的特性,就说 Rust 程序只要能跑起来,那代码质量其实就是相当不错的,因为 Rust 编译器、clippy 啥的实在是严师厉友,甚至有些鸡毛。 + +正因为这较高的质量下限,我们在 review 时并不用担心潜在的各种坑,因此可以实现快速的开发、review、merge 流程。 -而且由于Rust语言拥有异常强大的编译器和语言特性,因此Rust的代码天然就会比其它语言有更少的Bug,同时Rust拥有非常完善的工具链、最好的包管理工具,这些叠加在一起,决定了Rust非常适合大型开发者团队的协作开发。 +而且由于 Rust 语言拥有异常强大的编译器和语言特性,因此 Rust 的代码天然就会比其它语言有更少的 Bug。同时 Rust 拥有非常完善的工具链,最好的包管理工具,这些叠加在一起,决定了 Rust 非常适合大型开发者团队的协作开发。 -也许Rust在开发速度上不是最快的,但是从开发 + 维护的角度来看,这个成本绝对是各个语言中最小的之一,当然如果你的公司就追求做出来能用就行,那Rust确实有些灰姑娘的感觉。 +也许 Rust 在开发速度上不是最快的,但是从开发 + 维护的角度来看,这个成本在各个语言中绝对是很小的。当然,如果你的公司就追求做出来能用就行,那 Rust 确实有些灰姑娘的感觉。 -还有一点很重要,现在的Rust程序员往往拥有超出更出众的能力和学习自驱性,因此团队招到的人天然就保持了较高的底线,如果你有幸招到一个优秀的Rust程序员,那真是捡到宝了,他也会同时带动周围的人一起慢慢优秀(优秀的Rust程序员较好辨别,门槛低的语言就并没有那么好辨别)。总之,一个这样的程序员会给团队带来远超他薪资的潜在回报和长远收益。 +还有一点很重要,现在的 Rust 程序员往往拥有更出众的能力和学习自驱性,因此团队招到的人天然就保持了较高的底线。如果你有幸招到一个优秀的 Rust 程序员,那真是捡到宝了,他也会同时带动周围的人一起慢慢优秀(优秀的 Rust 程序员比较好辨别,门槛低的语言就并没有那么好辨别)。总之,一个这样的程序员会给团队带来远超他薪资的潜在回报和长远收益。 ### 开源 -目前Rust的主战场是在开源上,Go的成功也证明了农村包围城市的可行性。 -- UI层开发:Rust的WASM发展的如火如荼, 隐隐有王者风范,在JS的基础设施领域Rust也是如鱼得水, 例如`swc`、`deno`等,同时`nextjs`也是押宝Rust,可以说Rust在前端的成功完全是无心插柳柳成荫。 -- 基础设施层, 数据库、搜索引擎、网络设施、云原生等都在出现Rust的身影,而且还不少 -- 系统开发,目前linux已经将Rust列为即将支持的内核开发语言,是即C语言后第二门支持内核开发的语言,不过刚开始将主要支持驱动开发 -- 系统工具,现在最流行的就是用Rust重写之前C、C++写的一票系统工具,还都获得了挺高的关注和很好的效果, 例如 sd, exa, ripgrep, fd, bat等 -- 操作系统, Rust在开发的操作系统现在有好几个,其中最有名的可能就是谷歌的fushia, Rust在其中扮演非常重要的角色 -- 区块链,目前Rust和Go可以说各领风骚,未来Rust可能会一统江湖 +目前 Rust 的主战场是在开源上,Go 的成功也证明了农村包围城市的可行性。 -类似的还有很多,我们就不一一列举, 总之,现在有大量的项目在被用Rust重写,同时还有海量的项目在等待被重写,这些都是赚取star和认可的好机会,在其它语言杀成一片红海时,Rust还留了一大片蓝海等待大家的探索! +- UI 层开发,Rust 的 WASM 发展的如火如荼,隐隐有王者风范,在 JS 的基础设施领域,Rust 也是如鱼得水,例如`swc`、`deno`等。同时`nextjs`也是押宝 Rust,可以说 Rust 在前端的成功完全是无心插柳柳成荫。 +- 基础设施层,数据库、搜索引擎、网络设施、云原生等都在出现 Rust 的身影,而且还不少。 +- 系统开发,目前 linux 已经将 Rust 列为即将支持的内核开发语言,是既 C 语言后第二门支持内核开发的语言,不过刚开始将主要支持驱动开发。 +- 系统工具,现在最流行的就是用 Rust 重写之前 C、C++ 写的一票系统工具,还都获得了挺高的关注和很好的效果,例如 sd, exa, ripgrep, fd, bat 等。 +- 操作系统,现在在使用 Rust 开发的操作系统有好几个,其中最有名的可能就是谷歌的 Fuchsia,Rust 在其中扮演非常重要的角色 +- 区块链,目前 Rust 和 Go 可以说各领风骚,未来 Rust 可能会一统江湖 +类似的还有很多,我们就不一一列举。总之,现在有大量的项目在被用 Rust 重写,同时还有海量的项目在等待被重写,这些都是赚取 star 和认可的好机会。在其它语言杀成一片红海时,Rust 还留了一大片蓝海等待大家的探索! +### 相比其他语言 Rust 的优势 -### 相比其他语言Rust的优势 -由于篇幅有限,我们这里不会讲述详细的对比,就是简单介绍下Rust的优势。因此并不是说Rust就优于这些语言,大家轻喷:) +由于篇幅有限,我们这里不会讲述详细的对比,就是简单介绍下 Rust 的优势。并不是说 Rust 就优于这些语言,大家轻喷:) #### Go -Rust语言表达能力更强,性能更高,同时线程安全方面Rust也更强,不容易写出错误的代码,包管理Rust也更好,Go虽然在1.10版本后提供了包管理,但是目前还比不上Rust的。 + +Rust 语言表达能力更强,性能更高。同时线程安全方面 Rust 也更强,不容易写出错误的代码。包管理 Rust 也更好,Go 虽然在 1.10 版本后提供了包管理,但是目前还比不上 Rust 的。 #### C++ -与C++相比,Rust的性能相差无几,但是在安全性方面会更优,特别是使用第三方库时,Rust的严格要求会让第三方库的质量明显高很多。 -语言本身的学习,Rust的前中期学习曲线会更陡峭,但是对于未来使用场景和生态的学习,C++会更难、更复杂。 +与 C++ 相比,Rust 的性能相差无几,但是在安全性方面会更优,特别是使用第三方库时,Rust 的严格要求会让第三方库的质量明显高很多。 + +语言本身的学习,Rust 的前中期学习曲线会更陡峭,但是对于未来使用场景和生态的学习,C++ 会更难,更复杂。 #### Java -除了极少部分纯粹的数字计算性能,Rust的性能是全面领先于Java的,同时Rust占用内存小的多,因此实现同等规模的服务,Rust所需的硬件成本会显著降低。 + +除了极少部分纯粹的数字计算性能,Rust 的性能是全面领先于 Java 的。同时 Rust 占用内存小的多,因此实现同等规模的服务,Rust 所需的硬件成本会显著降低。 #### Python -性能自然是Rust完胜,同时Rust对运行环境要求较低,这两点差不多就足够抉择了,因为python和rust的彼此适用面其实不太冲突。 + +性能自然是 Rust 完胜,同时 Rust 对运行环境要求较低,这两点差不多就足够抉择了。不过 python 和 rust 的彼此适用面其实也不太冲突。 ### 使用现状 -- AWS从2017年开始就用Rust实现了无服务器计算平台: AWS Lambda 和 AWS Fargate, 并且用Rust重写了Bottlerocket OS和AWS Nitro系统,这两个是弹性计算云(EC2)的重要服务 -- Cloudflare是Rust的重度用户,DNS、无服务计算、网络包监控等基础设施都都与Rust密不可分 -- Dropbox的底层存储服务完全由Rust重写,达到了数万PB的规模 -- Google除了在安卓系统的部分模块中使用Rust外,还在它最新的操作系统fuchsia中重度使用Rust -- Facebook使用Rust来增强自己的网页端、移动端和API服务的性能,同时还写了Hack编程语言的虚拟机 -- Microsoft使用Rust为Azure平台提供一些组件,其中包括IoT的核心服务 -- Github和npmjs.com,使用Rust提供高达每天13亿次的npm包下载 -- Rust目前已经成为全世界区块链平台的首选开发语言 +- AWS 从 2017 年开始就用 Rust 实现了无服务器计算平台: AWS Lambda 和 AWS Fargate,并且用 Rust 重写了 Bottlerocket OS 和 AWS Nitro 系统,这两个是弹性计算云 (EC2) 的重要服务 +- Cloudflare 是 Rust 的重度用户,DNS、无服务计算、网络包监控等基础设施都都与 Rust 密不可分 +- Dropbox 的底层存储服务完全由 Rust 重写,达到了数万 PB 的规模 +- Google 除了在安卓系统的部分模块中使用 Rust 外,还在它最新的操作系统 Fuchsia 中重度使用 Rust +- Facebook 使用 Rust 来增强自己的网页端、移动端和 API 服务的性能,同时还写了 Hack 编程语言的虚拟机 +- Microsoft 使用 Rust 为 Azure 平台提供一些组件,其中包括 IoT 的核心服务 +- Github 和 npmjs.com,使用 Rust 提供高达每天 13 亿次的 npm 包下载 +- Rust 目前已经成为全世界区块链平台的首选开发语言 - Tidb,国内最有名的开源分布式数据库 -尤其值得一提的是,AWS实际上在押宝Rust,未来对Rust的使用可能很快会上升到**first-class**的地位。 - +尤其值得一提的是,AWS 实际上在押宝 Rust,未来对 Rust 的使用可能很快会上升到 **first-class** 的地位。 -## Rust语言版本更新 +## Rust 语言版本更新 -与其它语言相比,Rust的更新迭代较为频繁(得益于精心设计过的发布流程以及Rust语言开发者团队的严格管理): -- 每6周发布一个迭代版本 -- 2-3年发布一个新的大版本:Rust 2018 edition,Rust 2021 edtion +与其它语言相比,Rust 的更新迭代较为频繁(得益于精心设计过的发布流程以及 Rust 语言开发者团队的严格管理): +- 每 6 周发布一个迭代版本 +- 2-3 年发布一个新的大版本:Rust 2018 edition,Rust 2021 edtion 好处在于,可以满足不同的用户群体的需求: -- 对于活跃的Rust用户,他们总是能很快获取到新的语言内容,毕竟,尝鲜是技术爱好者的共同特点:) -- 对于一般的用户,edition大版本的发布会告诉他们:Rust语言相比上次大版本发布,有了重大的改进,值得一看 -- 对于Rust语言开发者,可以让他们的工作成果更快的被世人所知,不必锦衣夜行 - -好了,相信大家听了这么多Rust的优点,已经迫不及待想要开始学习旅程,那么容我引用一句CS的经典台词:OK,let's go. +- 对于活跃的 Rust 用户,他们总是能很快获取到新的语言内容,毕竟,尝鲜是技术爱好者的共同特点:) +- 对于一般的用户,edition 大版本的发布会告诉他们:Rust 语言相比上次大版本发布,有了重大的改进,值得一看 +- 对于 Rust 语言开发者,可以让他们的工作成果更快的被世人所知,不必锦衣夜行 +好了,相信大家听了这么多 Rust 的优点,已经迫不及待想要开始学习旅程,那么容我引用一句 CS 的经典台词:OK, let's go. ## 总结 -连续6年最受欢迎的语言当然不是浪得虚名。 无GC、**效率高**、**工程性强**、强安全性以及能同时得到工程派和学院派认可, 这些令Rust拥有了自己的特色和生存空间,社区的友善,生态的快速发展,大公司的重仓跟进,一切的一切都在说明Rust的未来。 -当然,语言毕竟还是工具,我们不能神话它,但是可以给它一个机会,也许你最终能收获自己的真爱:) \ No newline at end of file +连续 6 年最受欢迎的语言当然不是浪得虚名。 无 GC、效率高、工程性强、强安全性以及能同时得到工程派和学院派认可,这些令 Rust 拥有了自己的特色和生存空间。社区的友善,生态的快速发展,大公司的重仓跟进,一切的一切都在说明 Rust 的未来。 + +当然,语言毕竟只是工具,我们不能神话它,但是可以给它一个机会,也许,你最终能收获自己的真爱。:)