Merge branch 'sunface:main' into patch-2

pull/468/head
Colin 3 years ago committed by GitHub
commit 5e7d561ec1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -4,9 +4,8 @@
- 官方: [https://course.rs](https://course.rs)
- 国内镜像: [https://book.rust.team](https://book.rust.team)
- 知乎: [支持章节内目录跳转,很好用!](https://www.zhihu.com/column/c_1452781034895446017)
- 最近修订: 2022-02-22 新增 [Cargo使用指南 - Cargo.toml VS Cargo.lock](https://zhuanlan.zhihu.com/p/470815396)
- Rust版本 Rust edition 2021
- 配套练习题: [https://exercise.rs](https://github.com/sunface/rust-exercise)
- 最近修订: 2022-02-23 新增 [Cargo使用指南 - 花样引入依赖](https://zhuanlan.zhihu.com/p/471641290)
- QQ交流群1009730433
## 教程简介

@ -0,0 +1,4 @@
## 2022-02-24
- 进阶中的enum/整数类型转换、newtype章节被合并到**深入类型**目录中
- 将 newtype 中的 Sized/DST 内容拆分成单独的章节,并扩展了相应内容

@ -66,7 +66,10 @@
- [函数式编程: 闭包、迭代器](advance/functional-programing/intro.md)
- [闭包closure](advance/functional-programing/closure.md)
- [迭代器iterator](advance/functional-programing/iterator.md)
- [深入类型之newtype和Sized](advance/custom-type.md)
- [深入类型](advance/into-types/intro.md)
- [newtype 和 类型别名](advance/into-types/custom-type.md)
- [Sized 和不定长类型 DST](advance/into-types/sized.md)
- [枚举和整数](advance/into-types/enum-int.md)
- [智能指针](advance/smart-pointer/intro.md)
- [Box<T>堆对象分配](advance/smart-pointer/box.md)
- [Deref解引用](advance/smart-pointer/deref.md)
@ -86,8 +89,7 @@
- [实践应用多线程Web服务器 todo](advance/concurrency-with-threads/web-server.md)
- [全局变量](advance/global-variable.md)
- [错误处理](advance/errors.md)
- [进阶类型转换 doing](advance/converse/intro.md)
- [枚举和整数](advance/converse/enum-int.md)
- [Unsafe Rust](advance/unsafe/intro.md)
- [五种兵器](advance/unsafe/superpowers.md)
- [Macro宏编程](advance/macro.md)
@ -154,7 +156,7 @@
- [易混淆概念解析](confonding/intro.md)
- [切片和切片引用](confonding/slice.md)
<!-- - [String、&str 和 str](confonding/string.md) -->
- [String、&str 和 str](confonding/string.md)
- [原生指针、引用和智能指针 todo](confonding/pointer.md)
- [作用域、生命周期和 NLL todo](confonding/lifetime.md)
- [move、Copy和Clone todo](confonding/move-copy.md)

@ -4,14 +4,14 @@
并行和并发其实并不难,但是也给一些用户造成了困扰,因此我们专门开辟一个章节,用于讲清楚这两者的区别。
`Erlang` 之父[`Joe Armstrong`](https://en.wikipedia.org/wiki/Joe_Armstrong_(programmer))(伟大的异步编程先驱,开创一个时代的殿堂级计算机科学家,我还犹记得当年刚学到 `Erlang` 时的震撼respect用一张5岁小孩都能看到的图片解释了并发与并行的区别
`Erlang` 之父 [`Joe Armstrong`](https://en.wikipedia.org/wiki/Joe_Armstrong_(programmer))(伟大的异步编程先驱,开创一个时代的殿堂级计算机科学家,我还犹记得当年刚学到 `Erlang` 时的震撼respect用一张5岁小孩都能看到的图片解释了并发与并行的区别
<img alt="" src="https://pic1.zhimg.com/80/f37dd89173715d0e21546ea171c8a915_1440w.png" class="center" />
上图很直观的体现了:
- **并发(Concurrent)** 是多个队列使用同一个咖啡机然后两个队列轮换着使用未必是1:1轮换也可能是其它轮换规则最终每个人都能接到咖啡
- **并发(Concurrent)** 是多个队列使用同一个咖啡机,然后两个队列轮换着使用(未必是 1:1 轮换,也可能是其它轮换规则),最终每个人都能接到咖啡
- **并行(Parallel)** 是每个队列都拥有一个咖啡机,最终也是每个人都能接到咖啡,但是效率更高,因为同时可以有两个人在接咖啡
当然,我们还可以对比下串行:只有一个队列且仅使用一台咖啡机,哪怕前面那个人接咖啡时突然发呆了几分钟,后面的人也只能等他结束才能继续接。可能有读者有疑问了,从图片来看,并发也存在这个问题啊,前面的人发呆了几分钟不接咖啡怎么办?很简单,另外一个队列的人把他推开就行了,自己队友不能在背后开枪,但是其它队的可以:)
@ -19,17 +19,17 @@
在正式开始之前,先给出一个结论:**并发和并行都是对“多任务”处理的描述,其中并发是轮流处理,而并行是同时处理**。
## CPU 多核
现在的个人计算机动辄拥有十来个核心(M1 Max/Intel 12代),如果使用串行的方式那真是太低调了,因此我们把各种任务简单分成多个队列,每个队列都交给一个 CPU 核心去执行,当某个 CPU 核心没有任务时,它还能去其它核心的队列中偷任务(真·老黄牛),这样就实现了并行化处理。
现在的个人计算机动辄拥有十来个核心M1 Max/Intel 12代,如果使用串行的方式那真是太低调了,因此我们把各种任务简单分成多个队列,每个队列都交给一个 CPU 核心去执行,当某个 CPU 核心没有任务时,它还能去其它核心的队列中偷任务(真·老黄牛),这样就实现了并行化处理。
#### 单核心并发
那问题来了,在早期只有一个 CPU 核心时,我们的任务是怎么处理的呢?其实聪明的读者应该已经想到,是的,并发解君愁。当然,这里还得提到操作系统的多线程,正是操作系统多线程 + CPU核心才实现了现代化的多任务操作系统。
那问题来了,在早期只有一个 CPU 核心时,我们的任务是怎么处理的呢?其实聪明的读者应该已经想到,是的,并发解君愁。当然,这里还得提到操作系统的多线程,正是操作系统多线程 + CPU 核心,才实现了现代化的多任务操作系统。
在 OS 级别,多线程负责管理我们的任务队列,你可以简单认为一个线程管理着一个任务队列,然后线程之间还能根据空闲度进行任务调度。我们的程序只会跟 OS 线程打交道,并不关心 CPU 到底有多少个核心,真正关心的只是 OS当线程把任务交给 CPU 核心去执行时,如果只有一个 CPU 核心,那么它就只能同时处理一个任务。
相信大家都看出来了:**CPU 核心**对应的是上图的咖啡机,而**多个线程的任务队列**就对应的多个排队的队列,由于终受限于 CPU 核心数,每个队列每次只会有一个任务被处理。
和排队一样,假如某个任务执行时间过长,就会导致用户界面的假死(相信使用 Windows 的同学或多或少都碰到过假死的问题) 那么就需要 CPU 的任务调度了(真实 CPU 的调度很复杂,我们这里做了简化),有一个调度器会按照某些条件从队列中选择任务进行执行,并且当一个任务执行时间过长时,会强行切换该任务到后台中(或者放入任务队列,真实情况很复杂!),去执行新的任务。
和排队一样,假如某个任务执行时间过长,就会导致用户界面的假死(相信使用 Windows 的同学或多或少都碰到过假死的问题), 那么就需要 CPU 的任务调度了(真实 CPU 的调度很复杂,我们这里做了简化),有一个调度器会按照某些条件从队列中选择任务进行执行,并且当一个任务执行时间过长时,会强行切换该任务到后台中(或者放入任务队列,真实情况很复杂!),去执行新的任务。
不断这样的快速任务切换,对用户而言就实现了表面上的多任务同时处理,但是实际上最终也只有一个 CPU 核心在不停的工作。
@ -37,12 +37,12 @@
#### 多核心并行
当 CPU 核心增多到 `N` 时,那么同一时间就能有 `N` 个任务被处理,那么我们的并行度就是 `N`,相应的处理效率也变成了单核心的 `N`(实际情况并没有这么高)
当 CPU 核心增多到 `N` 时,那么同一时间就能有 `N` 个任务被处理,那么我们的并行度就是 `N`,相应的处理效率也变成了单核心的 `N`(实际情况并没有这么高)
#### 多核心并发
当核心增多到 `N` 时,操作系统同时在进行的任务肯定远不止 `N` 个,这些任务将被放入 `M` 个线程队列中,接着交给 `N` 个CPU核心去执行最后实现了 `M:N` 的处理模型,在这种情况下,**并发跟并行时同时在发生的,所有用户任务从表面来看都在并发的运行,其实实际上,同一时刻只有 `N` 个任务能被同时并行的处理**。
看到这里,相信大家已经明白两者的区别,那么我们下面给出一个正式的定义(该定义摘选自<<并发的艺术>>)
看到这里,相信大家已经明白两者的区别,那么我们下面给出一个正式的定义(该定义摘选自<<并发的艺术>>
## 正式的定义
@ -57,14 +57,14 @@
如果大家学过其它语言的多线程,可能就知道不同语言对于线程的实现可能大相径庭:
- 由于操作系统提供了创建线程的 API因此部分语言会直接调用该 API 来创建线程,因此最终程序内的线程数和该程序占用的操作系统线程数相等,一般称之为**1:1 线程模型**,例如 Rust。
- 还有些语言在内部实现了自己的线程模型(绿色线程、协程),程序内部的 M 个线程最后会以某种映射方式使用 N 个操作系统线程去运行,因此称之为**M:N 线程模型**,其中 M 和 N 并没有特定的彼此限制关系。一个典型的代表就是 Go 语言。
- 还有些语言在内部实现了自己的线程模型(绿色线程、协程),程序内部的 M 个线程最后会以某种映射方式使用 N 个操作系统线程去运行,因此称之为**M:N 线程模型**,其中 M 和 N 并没有特定的彼此限制关系。一个典型的代表就是 Go 语言。
- 还有些语言使用了 Actor 模型,基于消息传递进行并发,例如 Erlang 语言。
总之,每一种模型都有其优缺点及选择上的权衡,而 Rust 在设计时考虑的权衡就是运行时(Runtime)。出于 Rust 的系统级使用场景,且要保证调用 C 时的极致性能,它最终选择了尽量小的运行时实现。
> 运行时是那些会被打包到所有程序可执行文件中的 Rust 代码,根据每个语言的设计权衡,运行时虽然有大有小(例如 Go 语言由于实现了协程和 GC运行时相对就会更大一些),但是除了汇编之外,每个语言都拥有它。小运行时的其中一个好处在于最终编译出的可执行文件会相对较小,同时也让该语言更容易被其它语言引入使用。
> 运行时是那些会被打包到所有程序可执行文件中的 Rust 代码,根据每个语言的设计权衡,运行时虽然有大有小(例如 Go 语言由于实现了协程和 GC运行时相对就会更大一些,但是除了汇编之外,每个语言都拥有它。小运行时的其中一个好处在于最终编译出的可执行文件会相对较小,同时也让该语言更容易被其它语言引入使用。
而绿色线程/协程的实现会显著增大运行时的大小,因此 Rust 只在标准库中提供了 `1:1` 的线程模型,如果你愿意牺牲一些性能来换取更精确的线程控制以及更小的线程上下文切换成本,那么可以选择 `Rust` 中的 `M:N` 模型,这些模型由三方库提供了实现,例如大名鼎鼎的 `tokio`
而绿色线程/协程的实现会显著增大运行时的大小,因此 Rust 只在标准库中提供了 `1:1` 的线程模型,如果你愿意牺牲一些性能来换取更精确的线程控制以及更小的线程上下文切换成本,那么可以选择 Rust 中的 `M:N` 模型,这些模型由三方库提供了实现,例如大名鼎鼎的 `tokio`
在了解了并发和并行后,我们可以正式开始 Rust 的多线程之旅。

@ -3,6 +3,6 @@
可惜的是,在 Rust 中由于语言设计理念、安全、性能的多方面考虑,并没有采用 Go 语言大道至简的方式,而是选择了多线程与 `async/await` 相结合,优点是可控性更强、性能更高,缺点是复杂度并不低,当然这也是系统级语言的应有选择:**使用复杂度换取可控性和性能**。
不过,大家也不用担心,本书的目标就是降低 Rust 使用门槛,这个门槛自然也包括如何在 Rust 中进行异步并发编程,我们将从多线程以及 async/await两个方面去深入浅出地讲解首先从本章的多线程开始。
不过,大家也不用担心,本书的目标就是降低 Rust 使用门槛,这个门槛自然也包括如何在 Rust 中进行异步并发编程,我们将从多线程以及 `async/await` 两个方面去深入浅出地讲解,首先,从本章的多线程开始。
在本章,我们将深入讲解并发和并行的区别以及如何使用多线程进行 Rust 并发编程,那么先来看看何为并行与并发。

@ -1,2 +0,0 @@
# 进阶类型转换
Rust 是强类型语言,同时也是强安全语言,这些特性导致了 Rust 的类型转换注定比一般语言要更困难,再加上 Rust 的繁多的类型和类型转换特征,因此大家很难对这块内容了如指掌,为此我们专门整了一个专题来讨论 Rust 中那些不太容易的类型转换, 容易的请看[这一章](../basic/converse.md).

@ -199,91 +199,3 @@ fn main() {
神奇的事发生了,此处 `panic` 竟然通过了编译。难道这两个宏拥有不同的返回类型?
你猜的没错:`panic` 的返回值是 `!`,代表它决不会返回任何值,既然没有任何返回值,那自然不会存在分支类型不匹配的情况。
## 动态大小类型
读者大大们之前学过的几乎所有类型,都是固定大小的类型,包括集合 `Vec`、`String` 和 `HashMap` 等,而动态大小类型刚好与之相反:**编译器无法在编译期得知该类型值的大小,只有到了程序运行时,才能动态获知**。对于动态类型,我们使用 `DST`(dynamically sized types)或者 `unsized` 类型来称呼它。
上述的这些集合虽然底层数据可动态变化,感觉像是动态大小的类型。但是实际上,这些底层数据只是保存在堆上,在栈中还存有一个引用类型,该引用包含了集合的内存地址、元素数目、分配空间信息,通过这些信息,编译器对于该集合的实际大小了若指掌,因此它们依然是固定大小的类型。
现在给你一个挑战:想出一个动态类型。俺厚黑地说一句,估计大部分人都想不出这样的一个类型,就连我,如果不是查询着资料在写,估计也一时半会儿想不到一个。
考虑一下这个类型:`str`,感觉有点眼生?是的,它既不是 `String` 动态字符串,也不是 `&str` 字符串切片,而是一个 `str`。它是一个动态类型,同时还是 `String``&str` 的底层数据类型。 由于 `str` 是动态类型,因此它的大小直到运行期才知道,下面的代码会因此报错:
```rust
// error
let s1: str = "Hello there!";
let s2: str = "How's it going?";
// ok
let s3: &str = "on?"
```
Rust 需要明确地知道一个特定类型的值占据了多少内存空间,同时该类型的所有值都必须使用相同大小的内存。如果 Rust 允许我们使用这种动态类型,那么这两个 `str` 值就需要占用同样大小的内存,这显然是不现实的: `s1` 占用了12字节`s2` 占用了15字节总不至于为了满足同样的内存大小用空白字符去填补字符串吧
所以,我们只有一条路走,那就是给它们一个固定大小的类型:`&str`。那么为何字符串切片 `&str` 就是固定大小呢?因为它的引用存储在栈上,具有固定大小(类似指针),同时它指向的数据存储在堆中,也是已知的大小,再加上 `&str` 引用中包含有堆上数据内存地址、长度等信息,因此最终可以得出字符串切片是固定大小类型的结论。
`&str` 类似,`String` 字符串也是固定大小的类型。
正是因为 `&str` 的引用有了底层堆数据的明确信息,它才是固定大小类型。假设如果它没有这些信息呢?那它也将变成一个动态类型。因此,将动态数据固定化的秘诀就是**使用引用指向这些动态数据,然后在引用中存储相关的内存位置、长度等信息**。
我们之前已经见过,使用 `Box` 将一个没有固定大小的特征变成一个有固定大小的特征对象,那能否故技重施,将 `str` 封装成一个固定大小类型?留个悬念先,我们来看看 `Sized` 特征。
> Rust中最常见的 `DST` 类型: `str`、`[T]`、`dyn Trait`
#### Sized 特征
既然动态类型的问题这么大那么在使用泛型时Rust 如何保证我们的泛型参数是固定大小的类型呢?例如以下泛型函数:
```rust
fn generic<T>(t: T) {
// --snip--
}
```
该函数很简单就一个泛型参数T那么如何保证 `T` 是固定大小的类型?仔细回想下,貌似在之前的课程章节中,我们也没有做过任何事情去做相关的限制,那 `T` 怎么就成了固定大小的类型了?奥秘在于编译器自动帮我们加上了 `Sized` 特征约束:
```rust
fn generic<T: Sized>(t: T) {
// --snip--
}
```
在上面Rust 自动添加的特征约束 `T: Sized`,表示泛型函数只能用于一切实现了 `Sized` 特征的类型上,而**所有在编译时就能知道其大小的类型,都会自动实现 `Sized` 特征**,例如。。。。也没啥好例如的,你能想到的几乎类型都实现了 `Sized` 特征,除了上面那个坑坑的 `str`,哦,还有特征。
**每一个特征都是一个可以通过名称来引用的动态大小类型**。因此如果想把特征作为具体的类型来传递给函数,你必须将其转换成一个特征对象:诸如 `&dyn Trait` 或者 `Box<dyn Trait>` (还有 `Rc<dyn Trait>`)这些引用类型。
现在还有一个问题:假如想在泛型函数中使用动态数据类型怎么办?可以使用 `?Sized` 特征(不得不说这个命名方式很 Rusty竟然有点幽默)
```rust
fn generic<T: ?Sized>(t: &T) {
// --snip--
}
```
`?Sized` 特征用于表明类型 `T` 既有可能是固定大小的类型,也可能是动态大小的类型。还有一点要注意的是,函数参数类型从 `T` 变成了 `&T`,因为 `T` 可能是动态大小的,因此需要用一个固定大小的指针(引用)来包裹它。
#### Box<str>
在结束前,再来看看之前遗留的问题:使用 `Box` 可以将一个动态大小的特征变成一个具有固定大小的特征对象,能否故技重施,将 `str` 封装成一个固定大小类型?
先回想下,章节前面的内容介绍过该如何把一个动态大小类型转换成固定大小的类型: **使用引用指向这些动态数据,然后在引用中存储相关的内存位置、长度等信息**。
好的,根据这个,我们来一起推测。首先,`Box<str>` 使用了一个引用来指向 `str`,嗯,满足了第一个条件。但是第二个条件呢?`Box` 中有该 `str` 的长度信息吗?显然是 `No`。那为什么特征就可以变成特征对象?其实这个还蛮复杂的,简单来说,对于特征对象,编译器无需知道它具体是什么类型,只要知道它能调用哪几个方法即可,因此编译器帮我们实现了剩下的一切。
来验证下我们的推测:
```rust
fn main() {
let s1: Box<str> = Box::new("Hello there!" as str);
}
```
报错如下:
```
error[E0277]: the size for values of type `str` cannot be known at compilation time
--> src/main.rs:2:24
|
2 | let s1: Box<str> = Box::new("Hello there!" as str);
| ^^^^^^^^ doesn't have a size known at compile-time
|
= help: the trait `Sized` is not implemented for `str`
= note: all function arguments must have a statically known size
```
提示得很清晰,不知道 `str` 的大小,因此无法对其使用 `Box` 进行封装。

@ -0,0 +1,4 @@
# 深入类型
Rust 是强类型语言,同时也是强安全语言,这些特性导致了 Rust 的类型注定比一般语言要更深入也更困难。
本章将深入讲解一些进阶的 Rust 类型以及类型转换,希望大家喜欢。

@ -0,0 +1,122 @@
# Sized 和不定长类型 DST
在 Rust 中类型有多种抽象的分类方式,例如本书之前章节的:基本类型、集合类型、复合类型等。再比如说,如果从编译器何时能获知类型大小的角度出发,可以分成两类:
- 定长类型( sized ),这些类型的大小在编译时是已知的
- 不定长类型( unsized ),与定长类型相反,它的大小只有到了程序运行时才能动态获知,这种类型又被称之为 DST
首先,我们来深入看看何为 DST。
## 动态大小类型 DST
读者大大们之前学过的几乎所有类型,都是固定大小的类型,包括集合 `Vec`、`String` 和 `HashMap` 等,而动态大小类型刚好与之相反:**编译器无法在编译期得知该类型值的大小,只有到了程序运行时,才能动态获知**。对于动态类型,我们使用 `DST`(dynamically sized types)或者 `unsized` 类型来称呼它。
上述的这些集合虽然底层数据可动态变化,感觉像是动态大小的类型。但是实际上,**这些底层数据只是保存在堆上,在栈中还存有一个引用类型**,该引用包含了集合的内存地址、元素数目、分配空间信息,通过这些信息,编译器对于该集合的实际大小了若指掌,最最重要的是:**栈上的引用类型是固定大小的**,因此它们依然是固定大小的类型。
**正因为编译器无法在编译期获知类型大小,若你试图在代码中直接使用 DST 类型,将无法通过编译。**
现在给你一个挑战:想出几个 DST 类型。俺厚黑地说一句,估计大部分人都想不出这样的一个类型,就连我,如果不是查询着资料在写,估计也一时半会儿想不到一个。
先来看一个最直白的:
#### 试图创建动态大小的数组
```rust
fn my_function(n: usize) {
let array = [123; n];
}
```
以上代码就会报错(错误输出的内容并不是因为 DST但根本原因是类似的),因为 `n` 在编译期无法得知,而数组类型的一个组成部分就是长度,长度变为动态的,自然类型就变成了 unsized 。
#### 切片
切片也是一个典型的 DST 类型,具体详情参见另一篇文章: [易混淆的切片和切片引用](https://course.rs/confonding/slice.html)。
#### str
考虑一下这个类型:`str`,感觉有点眼生?是的,它既不是 `String` 动态字符串,也不是 `&str` 字符串切片,而是一个 `str`。它是一个动态类型,同时还是 `String``&str` 的底层数据类型。 由于 `str` 是动态类型,因此它的大小直到运行期才知道,下面的代码会因此报错:
```rust
// error
let s1: str = "Hello there!";
let s2: str = "How's it going?";
// ok
let s3: &str = "on?"
```
Rust 需要明确地知道一个特定类型的值占据了多少内存空间,同时该类型的所有值都必须使用相同大小的内存。如果 Rust 允许我们使用这种动态类型,那么这两个 `str` 值就需要占用同样大小的内存,这显然是不现实的: `s1` 占用了12字节`s2` 占用了15字节总不至于为了满足同样的内存大小用空白字符去填补字符串吧
所以,我们只有一条路走,那就是给它们一个固定大小的类型:`&str`。那么为何字符串切片 `&str` 就是固定大小呢?因为它的引用存储在栈上,具有固定大小(类似指针),同时它指向的数据存储在堆中,也是已知的大小,再加上 `&str` 引用中包含有堆上数据内存地址、长度等信息,因此最终可以得出字符串切片是固定大小类型的结论。
`&str` 类似,`String` 字符串也是固定大小的类型。
正是因为 `&str` 的引用有了底层堆数据的明确信息,它才是固定大小类型。假设如果它没有这些信息呢?那它也将变成一个动态类型。因此,将动态数据固定化的秘诀就是**使用引用指向这些动态数据,然后在引用中存储相关的内存位置、长度等信息**。
#### 特征对象
```rust
fn foobar_1(thing: &dyn MyThing) {} // OK
fn foobar_2(thing: Box<dyn MyThing>) {} // OK
fn foobar_3(thing: MyThing) {} // ERROR!
```
如上所示,只能通过引用或 `Box` 的方式来使用特征对象,直接使用将报错!
#### 总结:只能间接使用的 DST
Rust中常见的 `DST` 类型有: `str`、`[T]`、`dyn Trait`**它们都无法单独被使用,必须要通过引用或者 `Box` 来间接使用** 。
我们之前已经见过,使用 `Box` 将一个没有固定大小的特征变成一个有固定大小的特征对象,那能否故技重施,将 `str` 封装成一个固定大小类型?留个悬念先,我们来看看 `Sized` 特征。
## Sized 特征
既然动态类型的问题这么大那么在使用泛型时Rust 如何保证我们的泛型参数是固定大小的类型呢?例如以下泛型函数:
```rust
fn generic<T>(t: T) {
// --snip--
}
```
该函数很简单就一个泛型参数T那么如何保证 `T` 是固定大小的类型?仔细回想下,貌似在之前的课程章节中,我们也没有做过任何事情去做相关的限制,那 `T` 怎么就成了固定大小的类型了?奥秘在于编译器自动帮我们加上了 `Sized` 特征约束:
```rust
fn generic<T: Sized>(t: T) {
// --snip--
}
```
在上面Rust 自动添加的特征约束 `T: Sized`,表示泛型函数只能用于一切实现了 `Sized` 特征的类型上,而**所有在编译时就能知道其大小的类型,都会自动实现 `Sized` 特征**,例如。。。。也没啥好例如的,你能想到的几乎类型都实现了 `Sized` 特征,除了上面那个坑坑的 `str`,哦,还有特征。
**每一个特征都是一个可以通过名称来引用的动态大小类型**。因此如果想把特征作为具体的类型来传递给函数,你必须将其转换成一个特征对象:诸如 `&dyn Trait` 或者 `Box<dyn Trait>` (还有 `Rc<dyn Trait>`)这些引用类型。
现在还有一个问题:假如想在泛型函数中使用动态数据类型怎么办?可以使用 `?Sized` 特征(不得不说这个命名方式很 Rusty竟然有点幽默)
```rust
fn generic<T: ?Sized>(t: &T) {
// --snip--
}
```
`?Sized` 特征用于表明类型 `T` 既有可能是固定大小的类型,也可能是动态大小的类型。还有一点要注意的是,函数参数类型从 `T` 变成了 `&T`,因为 `T` 可能是动态大小的,因此需要用一个固定大小的指针(引用)来包裹它。
## Box<str>
在结束前,再来看看之前遗留的问题:使用 `Box` 可以将一个动态大小的特征变成一个具有固定大小的特征对象,能否故技重施,将 `str` 封装成一个固定大小类型?
先回想下,章节前面的内容介绍过该如何把一个动态大小类型转换成固定大小的类型: **使用引用指向这些动态数据,然后在引用中存储相关的内存位置、长度等信息**。
好的,根据这个,我们来一起推测。首先,`Box<str>` 使用了一个引用来指向 `str`,嗯,满足了第一个条件。但是第二个条件呢?`Box` 中有该 `str` 的长度信息吗?显然是 `No`。那为什么特征就可以变成特征对象?其实这个还蛮复杂的,简单来说,对于特征对象,编译器无需知道它具体是什么类型,只要知道它能调用哪几个方法即可,因此编译器帮我们实现了剩下的一切。
来验证下我们的推测:
```rust
fn main() {
let s1: Box<str> = Box::new("Hello there!" as str);
}
```
报错如下:
```
error[E0277]: the size for values of type `str` cannot be known at compilation time
--> src/main.rs:2:24
|
2 | let s1: Box<str> = Box::new("Hello there!" as str);
| ^^^^^^^^ doesn't have a size known at compile-time
|
= help: the trait `Sized` is not implemented for `str`
= note: all function arguments must have a statically known size
```
提示得很清晰,不知道 `str` 的大小,因此无法对其使用 `Box` 进行封装。

@ -1,7 +1,7 @@
# Cell 和 RefCell
Rust 的编译器之严格可以说是举世无双。特别是在所有权方面Rust 通过严格的规则来保证所有权和借用的正确性,最终为程序的安全保驾护航。
但是严格是一把双刃剑,带来安全提升的同时,损失了灵活性,有时甚至会让用户痛苦不堪、怨声载道。因此 Rust 提供了 `Cell``RefCell` 用于内部可变性,简而言之,可以在拥有不可变引用的同时修改目标数据,对于正常的代码实现来说,这个是不可能做到的(要么一个可变借用,要么多个不可变借用)
但是严格是一把双刃剑,带来安全提升的同时,损失了灵活性,有时甚至会让用户痛苦不堪、怨声载道。因此 Rust 提供了 `Cell``RefCell` 用于内部可变性,简而言之,可以在拥有不可变引用的同时修改目标数据,对于正常的代码实现来说,这个是不可能做到的(要么一个可变借用,要么多个不可变借用)
> 内部可变性的实现是因为 Rust 使用了 `unsafe` 来做到这一点,但是对于使用者来说,这些都是透明的,因为这些不安全代码都被封装到了安全的 API 中
@ -14,7 +14,7 @@ fn main() {
let one = c.get();
c.set("qwer");
let two = c.get();
println!("{},{}", one,two);
println!("{},{}", one, two);
}
```
@ -57,7 +57,7 @@ fn main() {
let s1 = s.borrow();
let s2 = s.borrow_mut();
println!("{},{}",s1,s2);
println!("{},{}", s1, s2);
}
```
@ -76,7 +76,7 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
`RefCell` 正是**用于你确信代码是正确的,而编译器却发生了误判时**。
对于大型的复杂程序,也可以选择使用 `RefCell` 来让事情简化。例如在 Rust 编译器的[`ctxt结构体`](https://github.com/rust-lang/rust/blob/620d1ee5346bee10ba7ce129b2e20d6e59f0377d/src/librustc/middle/ty.rs#L803-L987)中有大量的 `RefCell` 类型的 `map` 字段, 主要的原因是:这些 `map` 会被分散在各个地方的代码片段所广泛使用或修改。由于这种分散在各处的使用方式,导致了管理可变和不可变成为一件非常复杂的任务(甚至不可能),你很容易就碰到编译器抛出来的各种错误。而且 `RefCell` 的运行时错误在这种情况下也变得非常可爱:一旦有人做了不正确的使用,代码会 `panic`,然后告诉我们哪些借用冲突了。
对于大型的复杂程序,也可以选择使用 `RefCell` 来让事情简化。例如在 Rust 编译器的[`ctxt结构体`](https://github.com/rust-lang/rust/blob/620d1ee5346bee10ba7ce129b2e20d6e59f0377d/src/librustc/middle/ty.rs#L803-L987)中有大量的 `RefCell` 类型的 `map` 字段主要的原因是:这些 `map` 会被分散在各个地方的代码片段所广泛使用或修改。由于这种分散在各处的使用方式,导致了管理可变和不可变成为一件非常复杂的任务(甚至不可能),你很容易就碰到编译器抛出来的各种错误。而且 `RefCell` 的运行时错误在这种情况下也变得非常可爱:一旦有人做了不正确的使用,代码会 `panic`,然后告诉我们哪些借用冲突了。
总之,当你确信编译器误报但不知道该如何解决时,或者你有一个引用类型,需要被四处使用和修改然后导致借用关系难以管理时,都可以优先考虑使用 `RefCell`
@ -149,7 +149,7 @@ struct MsgQueue {
}
impl Messenger for MsgQueue {
fn send(&self,msg: String) {
fn send(&self, msg: String) {
self.msg_cache.push(msg)
}
}
@ -229,7 +229,7 @@ RefCell { value: "我很善变,还拥有多个主人, on yeah!" }
#### 性能损耗
相信这两者组合在一起使用时,很多人会好奇到底性能如何,下面我们来简单分析下。
首先给出一个大概的结论,这两者结合在一起使用的性能其实非常高,大致相当于没有线程安全版本的 C++ `std::shared_ptr` 指针,事实上,`C++` 这个指针的主要开销也在于原子性这个并发原语上,毕竟线程安全在哪个语言中开销都不小。
首先给出一个大概的结论,这两者结合在一起使用的性能其实非常高,大致相当于没有线程安全版本的 C++ `std::shared_ptr` 指针事实上C++ 这个指针的主要开销也在于原子性这个并发原语上,毕竟线程安全在哪个语言中开销都不小。
#### 内存损耗
两者结合的数据结构与下面类似:
@ -252,28 +252,28 @@ struct Wrapper<T> {
#### CPU 损耗
从CPU来看损耗如下
- 对 `Rc<T>` 解引用是免费的(编译期),但是*带来的间接取值并不免费
- 克隆 `Rc<T>` 需要将当前的引用计数跟 `0``usize::Max` 进行一次比较然后将计数值加1
- 释放 (drop)`Rc<T>` 需要将计数值减1 然后跟 `0` 进行一次比较
- 对 `Rc<T>` 解引用是免费的(编译期),但是 `*` 带来的间接取值并不免费
- 克隆 `Rc<T>` 需要将当前的引用计数跟 `0``usize::Max` 进行一次比较,然后将计数值加 1
- 释放drop `Rc<T>` 需要将计数值减 1 然后跟 `0` 进行一次比较
- 对 `RefCell` 进行不可变借用,需要将 `isize` 类型的借用计数加1然后跟 `0` 进行比较
- 对 `RefCell `的不可变借用进行释放,需要将 `isize` 减1
- 对 `RefCell` 的可变借用大致流程跟上面差不多,但是需要先跟 `0` 比较然后再减1
- 对 `RefCell` 的可变借用进行释放,需要将 `isize` 加1
- 对 `RefCell `的不可变借用进行释放,需要将 `isize` 1
- 对 `RefCell` 的可变借用大致流程跟上面差不多,但是需要先跟 `0` 比较,然后再减 1
- 对 `RefCell` 的可变借用进行释放,需要将 `isize` 1
其实这些细节不必过于关注,只要知道 `CPU` 消耗也非常低,甚至编译器还会对此进行进一步优化!
其实这些细节不必过于关注,只要知道 CPU 消耗也非常低,甚至编译器还会对此进行进一步优化!
#### CPU 缓存 Miss
唯一需要担心的可能就是这种组合数据结构对于 `CPU` 缓存是否亲和,这个我们无法证明,只能提出来存在这个可能性,最终的性能影响还需要在实际场景中进行测试。
唯一需要担心的可能就是这种组合数据结构对于 CPU 缓存是否亲和,这个我们无法证明,只能提出来存在这个可能性,最终的性能影响还需要在实际场景中进行测试。
总之,分析这两者组合的性能还挺复杂的,大概总结下:
- 从表面来看,它们带来的内存和 CPU 损耗都不大
- 但是由于 `Rc` 额外的引入了一次间接取值(*),在少数场景下可能会造成性能上的显著损失
- 但是由于 `Rc` 额外的引入了一次间接取值`*`,在少数场景下可能会造成性能上的显著损失
- CPU 缓存可能也不够亲和
## 通过 `Cell::from_mut` 解决借用冲突
在 Rust1.37 版本中新增了两个非常实用的方法:
在 Rust 1.37 版本中新增了两个非常实用的方法:
- Cell::from_mut该方法将 `&mut T` 转为 `&Cell<T>`
- Cell::as_slice_of_cells该方法将 `&Cell<[T]>` 转为 `&[Cell<T>]`

@ -32,7 +32,7 @@ fn main() {
let b = Rc::clone(&a);
assert_eq!(2, Rc::strong_count(&a));
assert_eq!(Rc::strong_count(&a),Rc::strong_count(&b))
assert_eq!(Rc::strong_count(&a), Rc::strong_count(&b))
}
```
@ -114,12 +114,15 @@ fn main() {
drop(gadget_owner);
// 尽管在上面我们释放了 gadget_owner但是依然可以在这里使用 owner 的信息
// 原因是在 drop 之前,存在三个指向 Gadget Man 的智能指针引用,上面仅仅 drop 掉其中一个智能指针引用,而不是 drop 掉 owner 数据,外面还有两个引用指向底层的 owner 数据,引用计数尚未清零
// 原因是在 drop 之前,存在三个指向 Gadget Man 的智能指针引用,上面仅仅
// drop 掉其中一个智能指针引用,而不是 drop 掉 owner 数据,外面还有两个
// 引用指向底层的 owner 数据,引用计数尚未清零
// 因此 owner 数据依然可以被使用
println!("Gadget {} owned by {}", gadget1.id, gadget1.owner.name);
println!("Gadget {} owned by {}", gadget2.id, gadget2.owner.name);
// 在函数最后,`gadget1` 和 `gadget2` 也被释放,最终引用计数归零,随后底层数据也被清理释放
// 在函数最后,`gadget1` 和 `gadget2` 也被释放,最终引用计数归零,随后底层
// 数据也被清理释放
}
```
@ -145,7 +148,7 @@ fn main() {
for _ in 0..10 {
let s = Rc::clone(&s);
let handle = thread::spawn(move || {
println!("{}",s)
println!("{}", s)
});
}
}
@ -182,7 +185,7 @@ fn main() {
for _ in 0..10 {
let s = Arc::clone(&s);
let handle = thread::spawn(move || {
println!("{}",s)
println!("{}", s)
});
}
}

@ -427,7 +427,7 @@ impl SimpleFuture for SocketRead<'_> {
关于第二点,其中一个简单粗暴的方法就是使用一个新线程不停的检查 `socket` 中是否有了数据,当有了后,就调用 `wake()` 函数。该方法确实可以满足需求,但是性能着实太低了,需要为每个阻塞的 `Future` 都创建一个单独的线程!
在现实世界中,该问题往往是通过操作系统提供的 `IO` 多路复用机制来完成,例如 `Linux` 中的 **`epoll`**`FreeBSD` 和 `MacOS` 中的 **`kqueue`** `Windows` 中的 **`IOCP`**, `Fuchisa`中的 **`ports`** 等(可以通过 Rust 的跨平台包 `mio` 来使用它们)。借助IO多路复用机制可以实现一个线程同时阻塞地去等待多个异步IO事件一旦某个事件完成就立即退出阻塞并返回数据。相关实现类似于以下代码
在现实世界中,该问题往往是通过操作系统提供的 `IO` 多路复用机制来完成,例如 `Linux` 中的 **`epoll`**`FreeBSD` 和 `macOS` 中的 **`kqueue`** `Windows` 中的 **`IOCP`**, `Fuchisa`中的 **`ports`** 等(可以通过 Rust 的跨平台包 `mio` 来使用它们)。借助IO多路复用机制可以实现一个线程同时阻塞地去等待多个异步IO事件一旦某个事件完成就立即退出阻塞并返回数据。相关实现类似于以下代码
```rust
struct IoBlocker {
/* ... */

@ -17,7 +17,7 @@ fn add(i: i32, j: i32) -> i32 {
当你看懂了这张图,其实就等于差不多完成了函数章节的学习,但是这么短的章节显然对不起读者老爷们的厚爱,所以我们来展开下。
## 函数要点
- 函数名和变量名使用[蛇形命名法(snake case)](../../practice/style-guide/naming.md),例如 `fn add_two() -> {}`
- 函数名和变量名使用[蛇形命名法(snake case)](https://course.rs/practice/naming.html),例如 `fn add_two() -> {}`
- 函数的位置可以随便放Rust 不关心我们在哪里定义了函数,只要有定义即可
- 每个函数参数都需要标注类型

@ -169,7 +169,7 @@ struct ChangeColorMessage(i32, i32, i32); // 元组结构体
例如我们有一个 WEB 服务,需要接受用户的长连接,假设连接有两种:`TcpStream` 和 `TlsStream`,但是我们希望对这两个连接的处理流程相同,也就是用同一个函数来处理这两个连接,代码如下:
```rust
func new (stream: TcpStream) {
fn new (stream: TcpStream) {
let mut s = stream;
if tls {
s = negotiate_tls(stream)

@ -91,7 +91,7 @@ error: a bin target must be available for `cargo run`
- 默认二进制包:`src/main.rs`,编译后生成的可执行文件与`package`同名
- 其余二进制包:`src/bin/main1.rs` 和 `src/bin/main2.rs`,它们会分别生成一个文件同名的二进制可执行文件
- 集成测试文件:`tests` 目录下
- 性能测试benchmark文件`benches` 目录下
- 基准性能测试 `benchmark` 文件:`benches` 目录下
- 项目示例:`examples` 目录下

@ -12,7 +12,7 @@
## 变量命名
在命名方面,和其它语言没有区别,不过当给变量命名时,需要遵循 [Rust 命名规范](../practice/style-guide/naming.md)。
在命名方面,和其它语言没有区别,不过当给变量命名时,需要遵循 [Rust 命名规范](https://course.rs/practice/naming.html)。
> Rust 语言有一些**关键字***keywords*),和其他语言一样,这些关键字都是被保留给 Rust 语言使用的,因此,它们不能被用作变量或函数的名称。在 [附录 A](../appendix/keywords) 中可找到关键字列表。

@ -22,7 +22,7 @@ time = "0.1.12"
> npm 使用的就是 `semver` 版本号,从 JS 过来的同学应该非常熟悉。
#### `^`
#### `^` 指定版本
与之前的 `"0.1.12"` 不同, `^` 可以指定一个版本号范围,**然后会使用该范围内的最大版本号来引用对应的包**。
只要新的版本号没有修改最左边的非零数字,那该版本号就在允许的版本号范围中。例如 `"^0.1.12"` 最左边的非零数字是 `1`,因此,只要新的版本号是 `"0.1.z"` 就可以落在范围内,而`0.2.0` 显然就没有落在范围内,因此通过 `"^0.1.12"` 引入的依赖包是无法被升级到 `0.2.0` 版本的。
@ -43,7 +43,7 @@ time = "0.1.12"
以上是更多的例子,**事实上,这个规则跟 `SemVer` 还有所不同**,因为对于 `SemVer` 而言,`0.x.y` 的版本是没有其它版本与其兼容的,而对于 Rust只要版本号 `0.x.y` 满足 `z>=y``x>0` 的条件,那它就能更新到 `0.x.z` 版本。
#### `~`
#### `~` 指定版本
`~` 指定了最小化版本 :
```rust
~1.2.3 := >=1.2.3, <1.3.0
@ -51,7 +51,7 @@ time = "0.1.12"
~1 := >=1.0.0, <2.0.0
```
#### `*`
#### `*` 通配符
这种方式允许将 `*` 所在的位置替换成任何数字:
```rust
* := >=0.0.0

@ -10,7 +10,7 @@
下面继续简单介绍下 VSCode以下内容引用于官网
> Visual Studio Code(VSCode) 是微软 2015 年推出的一个轻量但功能强大的源代码编辑器,基于 Electron 开发,支持 Windows、Linux 和 MacOS 操作系统。它内置了对 JavaScriptTypeScript 和 Node.js 的支持并且具有丰富的其它语言和扩展的支持功能超级强大。Visual Studio Code 是一款免费开源的现代化轻量级代码编辑器,支持几乎所有主流的开发语言的语法高亮、智能代码补全、自定义快捷键、括号匹配和颜色区分、代码片段、代码对比 Diff、GIT 命令等特性,支持插件扩展,并针对网页开发和云端应用开发做了优化。
> Visual Studio Code(VSCode) 是微软 2015 年推出的一个轻量但功能强大的源代码编辑器,基于 Electron 开发,支持 Windows、Linux 和 macOS 操作系统。它内置了对 JavaScriptTypeScript 和 Node.js 的支持并且具有丰富的其它语言和扩展的支持功能超级强大。Visual Studio Code 是一款免费开源的现代化轻量级代码编辑器,支持几乎所有主流的开发语言的语法高亮、智能代码补全、自定义快捷键、括号匹配和颜色区分、代码片段、代码对比 Diff、GIT 命令等特性,支持插件扩展,并针对网页开发和云端应用开发做了优化。
## 安装 VSCode 的 Rust 插件

@ -9,7 +9,7 @@
至于版本,现在 Rust 稳定版特性越来越全了,因此下载最新稳定版本即可。由于你用的 Rust 版本可能跟本书写作时不一样,一些编译错误和警告可能也会有所不同。
## 在 Linux 或 MacOS 上安装 `rustup`
## 在 Linux 或 macOS 上安装 `rustup`
打开终端并输入下面命令:
@ -31,7 +31,7 @@ OK这样就已经完成 Rust 安装啦。
Rust 对运行环境的依赖和 Go 语言很像几乎所有环境都可以无需安装任何依赖直接运行。但是Rust 会依赖 `libc` 和链接器 `linker`。所以如果遇到了提示链接器无法执行的错误,你需要再手动安装一个 C 语言编译器:
**MacOS 下:**
**macOS 下:**
```console
$ xcode-select --install

@ -4,7 +4,7 @@
在本章中,你将学习以下内容:
1. 在 MacOS、Linux、Windows 上安装 Rust 以及相关工具链
1. 在 macOS、Linux、Windows 上安装 Rust 以及相关工具链
2. 搭建 VSCode 所需的环境
3. 简单介绍 Cargo
4. 实现一个酷炫多国语言版本的“世界,你好”的程序,并且谈谈对 Rust 语言的初印象

@ -3,7 +3,7 @@
## 开始使用
_注意: 如果你在使用 MacOS确保已经安装了 Xcode 以及相应的开发者工具 `xcode-select --install`._
_注意: 如果你在使用 macOS确保已经安装了 Xcode 以及相应的开发者工具 `xcode-select --install`._
同时你也需要安装Rust具体参见<<精通Rust编程>>一书或者访问https://rustup.rs。

Loading…
Cancel
Save