Update(advance): unified format 2

pull/509/head
Allan Downey 3 years ago
parent 48168d3168
commit 73d8f7df47

@ -1,8 +1,11 @@
# Weak 与循环引用
Rust 的安全性是众所周知的,但是不代表它不会内存泄漏。一个典型的例子就是同时使用 `Rc<T>``RefCell<T>` 创建循环引用,最终这些引用的计数都无法被归零,因此 `Rc<T>` 拥有的值也不会被释放清理。
## 何为循环引用
关于内存泄漏,如果你没有充足的 Rust 经验,可能都无法造出一份代码来再现它:
```rust
use crate::List::{Cons, Nil};
use std::cell::RefCell;
@ -33,6 +36,7 @@ fn main() {}
如上图所示,每个矩形框节点都是一个 `List` 类型,它们或者是拥有值且指向另一个 `List` 的`Cons`,或者是一个没有值的终结点 `Nil`。同时,由于 `RefCell` 的使用,每个 `List` 所指向的 `List` 还能够被修改。
下面来使用一下这个复杂的 `List` 枚举:
```rust
fn main() {
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
@ -46,7 +50,7 @@ fn main() {
println!("在b创建后a的rc计数 = {}", Rc::strong_count(&a));
println!("b的初始化rc计数 = {}", Rc::strong_count(&b));
println!("b指向的节点 = {:?}", b.tail());
// 利用RefCell的可变性创建了`a`到`b`的引用
if let Some(link) = a.tail() {
*link.borrow_mut() = Rc::clone(&b);
@ -62,12 +66,14 @@ fn main() {
```
这个类型定义看着复杂,使用起来更复杂!不过排除这些因素,我们可以清晰看出:
1. 在创建了 `a` 后,紧接着就使用 `a` 创建了 `b`,因此 `b` 引用了 `a`
2. 然后我们又利用 `Rc` 克隆了 `b`,然后通过 `RefCell` 的可变性,让 `a` 引用了 `b`
至此我们成功创建了循环引用`a`-> `b` -> `a` -> `b` ····
先来观察下引用计数:
```console
a的初始化rc计数 = 1
a指向的节点 = Some(RefCell { value: Nil })
@ -84,8 +90,9 @@ b指向的节点 = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
<img alt="" src="https://pic1.zhimg.com/80/v2-2dbfc981f05019bf70bf81c93f956c35_1440w.png" class="center" />
现在我们还需要轻轻的推一下,让塔米诺骨牌轰然倒塌。反注释最后一行代码,试着运行下:
```console
RefCell { value: Cons(5, RefCell { value: Cons(10, RefCell { value: Cons(5, RefCell { value: Cons(10, RefCell { value: Cons(5, RefCell { value: Cons(10, RefCell {
RefCell { value: Cons(5, RefCell { value: Cons(10, RefCell { value: Cons(5, RefCell { value: Cons(10, RefCell { value: Cons(5, RefCell { value: Cons(10, RefCell {
...无穷无尽
thread 'main' has overflowed its stack
fatal runtime error: stack overflow
@ -100,6 +107,7 @@ fatal runtime error: stack overflow
那么问题来了? 如果我们确实需要实现上面的功能,该怎么办?答案是使用 `Weak`
## Weak
`Weak` 非常类似于 `Rc`,但是与 `Rc` 持有所有权不同,`Weak` 不持有所有权,它仅仅保存一份指向数据的弱引用:如果你想要访问数据,需要通过 `Weak` 指针的 `upgrade` 方法实现,该方法返回一个类型为 `Option<Rc<T>>` 的值。
看到这个返回,相信大家就懂了:何为弱引用?就是**不保证引用关系依然存在**,如果不存在,就返回一个 `None`
@ -107,14 +115,15 @@ fatal runtime error: stack overflow
因为 `Weak` 引用不计入所有权,因此它**无法阻止所引用的内存值被释放掉**,而且 `Weak` 本身不对值的存在性做任何担保,引用的值还存在就返回 `Some`,不存在就返回 `None`
#### Weak 与 Rc 对比
我们来将 `Weak``Rc` 进行以下简单对比:
| `Weak` | `Rc` |
|--------|-------------|
| 不计数 | 引用计数 |
| 不拥有所有权 | 拥有值的所有权 |
| 不阻止值被释放(drop) | 所有权计数归零,才能 drop |
| 引用的值存在返回 `Some`,不存在返回 `None ` | 引用的值必定存在 |
| `Weak` | `Rc` |
| ----------------------------------------------- | ----------------------------------------- |
| 不计数 | 引用计数 |
| 不拥有所有权 | 拥有值的所有权 |
| 不阻止值被释放(drop) | 所有权计数归零,才能 drop |
| 引用的值存在返回 `Some`,不存在返回 `None ` | 引用的值必定存在 |
| 通过 `upgrade` 取到 `Option<Rc<T>>`,然后再取值 | 通过 `Deref` 自动解引用,取值无需任何操作 |
通过这个对比,可以非常清晰的看出 `Weak` 为何这么弱,而这种弱恰恰非常适合我们实现以下的场景:
@ -125,6 +134,7 @@ fatal runtime error: stack overflow
使用方式简单总结下:**对于父子引用关系,可以让父节点通过 `Rc` 来引用子节点,然后让子节点通过 `Weak` 来引用父节点**。
#### Weak 总结
因为 `Weak` 本身并不是很好理解,因此我们再来帮大家梳理总结下,然后再通过一个例子,来彻底掌握。
`Weak` 通过 `use std::rc::Weak` 来引入,它具有以下特点:
@ -135,6 +145,7 @@ fatal runtime error: stack overflow
- 常用于解决循环引用的问题
一个简单的例子:
```rust
use std::rc::Rc;
fn main() {
@ -160,10 +171,13 @@ fn main() {
需要承认的是,使用 `Weak` 让 Rust 本来就堪忧的代码可读性又下降了不少,但是。。。真香,因为可以解决循环引用了。
## 使用 Weak 解决循环引用
理论知识已经足够,现在用两个例子来模拟下真实场景下可能会遇到的循环引用。
#### 工具间的故事
工具间里,每个工具都有其主人,且多个工具可以拥有一个主人;同时一个主人也可以拥有多个工具,在这种场景下,就很容易形成循环引用,好在我们有 `Weak`
```rust
use std::rc::Rc;
use std::rc::Weak;
@ -220,6 +234,7 @@ fn main() {
```
#### tree 数据结构
```rust
use std::cell::RefCell;
use std::rc::{Rc, Weak};
@ -279,15 +294,16 @@ fn main() {
这个例子就留给读者自己解读和分析,我们就不画蛇添足了:
## unsafe 解决循环引用
除了使用 Rust 标准库提供的这些类型,你还可以使用 `unsafe` 里的原生指针来解决这些棘手的问题,但是由于我们还没有讲解 `unsafe`,因此这里就不进行展开,只附上[源码链接](https://github.com/sunface/rust-algos/blob/fbcdccf3e8178a9039329562c0de0fd01a3372fb/src/unsafe/self-ref.md), 挺长的需要耐心o_o
除了使用 Rust 标准库提供的这些类型,你还可以使用 `unsafe` 里的原生指针来解决这些棘手的问题,但是由于我们还没有讲解 `unsafe`,因此这里就不进行展开,只附上[源码链接](https://github.com/sunface/rust-algos/blob/fbcdccf3e8178a9039329562c0de0fd01a3372fb/src/unsafe/self-ref.md), 挺长的,需要耐心 o_o
虽然 `unsafe` 不安全,但是在各种库的代码中依然很常见用它来实现自引用结构,主要优点如下:
- 性能高,毕竟直接用原生指针操作
- 代码更简单更符合直觉: 对比下 `Option<Rc<RefCell<Node>>>`
## 总结
本文深入讲解了何为循环引用以及如何使用 `Weak` 来解决,同时还结合 `Rc`、`RefCell`、`Weak` 等实现了两个有实战价值的例子,让大家对智能指针的使用更加融会贯通。
至此,智能指针一章即将结束(严格来说还有一个 `Mutex` 放在多线程一章讲解),而 Rust 语言本身的学习之旅也即将结束,后面我们将深入多线程、项目工程、应用实践、性能分析等特色专题,来一睹 Rust 在这些领域的风采。

@ -1,4 +1,5 @@
# 循环引用与自引用
实现一个链表是学习各大编程语言的常用技巧,但是在 Rust 中实现链表意味着····Hell是的你没看错Welcome to hell。
链表在 Rust 中之所以这么难,完全是因为循环引用和自引用的问题引起的,这两个问题可以说综合了 Rust 的很多难点,难出了新高度,因此本书专门开辟一章,分为上下两篇,试图彻底解决这两个老大难。

@ -1,7 +1,9 @@
## 结构体自引用
结构体自引用在 Rust 中是一个众所周知的难题,而且众说纷纭,也没有一篇文章能把相关的话题讲透,那本文就王婆卖瓜,来试试看能不能讲透这一块儿内容,让读者大大们舒心。
## 平平无奇的自引用
可能也有不少人第一次听说自引用结构体,那咱们先来看看它们长啥样。
```rust
@ -12,7 +14,9 @@ struct SelfRef<'a> {
pointer_to_value: &'a str,
}
```
以上就是一个很简单的自引用结构体,看上去好像没什么,那来试着运行下:
```rust
fn main(){
let s = "aaa".to_string();
@ -24,6 +28,7 @@ fn main(){
```
运行后报错:
```console
let v = SelfRef {
12 | value: s,
@ -35,7 +40,9 @@ fn main(){
因为我们试图同时使用值和值的引用,最终所有权转移和借用一起发生了。所以,这个问题貌似并没有那么好解决,不信你可以回想下自己具有的知识,是否可以解决?
## 使用 Option
最简单的方式就是使用 `Opiton` 分两步来实现:
```rust
#[derive(Debug)]
struct WhatAboutThis<'a> {
@ -55,6 +62,7 @@ fn main() {
```
在某种程度上来说,`Option` 这个方法可以工作,但是这个方法的限制较多,例如从一个函数创建并返回它是不可能的:
```rust
fn creator<'a>() -> WhatAboutThis<'a> {
let mut tricky = WhatAboutThis {
@ -68,6 +76,7 @@ fn creator<'a>() -> WhatAboutThis<'a> {
```
报错如下:
```console
error[E0515]: cannot return value referencing local data `tricky.name`
--> src/main.rs:24:5
@ -82,6 +91,7 @@ error[E0515]: cannot return value referencing local data `tricky.name`
其实从函数签名就能看出来端倪,`'a` 生命周期是凭空产生的!
如果是通过方法使用,你需要一个无用 `&'a self` 生命周期标识,一旦有了这个标识,代码将变得更加受限,你将很容易就获得借用错误,就连 NLL 规则都没用:
```rust
#[derive(Debug)]
struct WhatAboutThis<'a> {
@ -108,7 +118,9 @@ fn main() {
```
## unsafe 实现
既然借用规则妨碍了我们,那就一脚踢开:
```rust
#[derive(Debug)]
struct SelfRef {
@ -151,6 +163,7 @@ fn main() {
在这里,我们在 `pointer_to_value` 中直接存储原生指针,而不是 Rust 的引用,因此不再受到 Rust 借用规则和生命周期的限制,而且实现起来非常清晰、简洁。但是缺点就是,通过指针获取值时需要使用 `unsafe` 代码。
当然,上面的代码你还能通过原生指针来修改 `String`,但是需要将 `*const` 修改为 `*mut`
```rust
#[derive(Debug)]
struct SelfRef {
@ -194,7 +207,9 @@ fn main() {
println!("{}, {:p}", t.value(), t.pointer_to_value());
}
```
运行后输出:
```console
hello, 0x16f3aec70
hello, world!, 0x16f3aec70
@ -203,9 +218,11 @@ hello, world!, 0x16f3aec70
上面的 `unsafe` 虽然简单好用,但是它不太安全,是否还有其他选择?还真的有,那就是 `Pin`
## 无法被移动的 Pin
`Pin` 在后续章节会深入讲解,目前你只需要知道它可以固定住一个值,防止该值在内存中被移动。
通过开头我们知道,自引用最麻烦的就是创建引用的同时,值的所有权会被转移,而通过 `Pin` 就可以很好的防止这一点:
```rust
use std::marker::PhantomPinned;
use std::pin::Pin;
@ -257,10 +274,10 @@ fn main() {
其实 `Pin` 在这里并没有魔法,它也并不是实现自引用类型的主要原因,最关键的还是里面的原生指针的使用,而 `Pin` 起到的作用就是确保我们的值不会被移走,否则指针就会指向一个错误的地址!
## 使用 ouroboros
对于自引用结构体,三方库也有支持的,其中一个就是 [ouroboros](https://github.com/joshua-maros/ouroboros),当然它也有自己的限制,我们后面会提到,先来看看该如何使用:
```rust
use ouroboros::self_referencing;
@ -292,6 +309,7 @@ fn main(){
在使用时,通过 `borrow_value` 来借用 `value` 的值,通过 `borrow_pointer_to_value` 来借用 `pointer_to_value` 这个指针。
看上去很美好对吧?但是你可以尝试着去修改 `String` 字符串的值试试,`ouroboros` 限制还是较多的,但是对于基本类型依然是支持的不错,以下例子来源于官方:
```rust
use ouroboros::self_referencing;
@ -337,6 +355,7 @@ fn main() {
只能说,它确实帮助我们解决了问题,但是一个是破坏了原有的结构,另外就是并不是所有数据类型都支持:它需要目标值的内存地址不会改变,因此 `Vec` 动态数组就不适合因为当内存空间不够时Rust 会重新分配一块空间来存放该数组,这会导致内存地址的改变。
类似的库还有:
- [rental](https://github.com/jpernst/rental) 这个库其实是最有名的,但是好像不再维护了,用倒是没问题
- [owning-ref](https://github.com/Kimundi/owning-ref-rs),将所有者和它的引用绑定到一个封装类型
@ -349,14 +368,15 @@ fn main() {
类似于循环引用的解决方式,自引用也可以用这种组合来解决,但是会导致代码的类型标识到处都是,大大的影响了可读性。
## 终极大法
如果两个放在一起会报错,那就分开它们。对,终极大法就这么简单,当然思路上的简单不代表实现上的简单,最终结果就是导致代码复杂度的上升。
如果两个放在一起会报错,那就分开它们。对,终极大法就这么简单,当然思路上的简单不代表实现上的简单,最终结果就是导致代码复杂度的上升。
## 学习一本书:如何实现链表
最后,推荐一本专门将如何实现链表的书(真是富有 Rust 特色链表都能复杂到出书了o_o[Learn Rust by writing Entirely Too Many Linked Lists](https://rust-unofficial.github.io/too-many-lists/)
最后,推荐一本专门将如何实现链表的书(真是富有 Rust 特色,链表都能复杂到出书了 o_o[Learn Rust by writing Entirely Too Many Linked Lists](https://rust-unofficial.github.io/too-many-lists/)
## 总结
上面讲了这么多方法,但是我们依然无法正确的告诉你在某个场景应该使用哪个方法,这个需要你自己的判断,因为自引用实在是过于复杂。
我们能做的就是告诉你,有这些办法可以解决自引用问题,而这些办法每个都有自己适用的范围,需要你未来去深入的挖掘和发现。

@ -4,11 +4,10 @@
并行和并发其实并不难,但是也给一些用户造成了困扰,因此我们专门开辟一个章节,用于讲清楚这两者的区别。
`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 轮换,也可能是其它轮换规则),最终每个人都能接到咖啡
@ -19,28 +18,30 @@
在正式开始之前,先给出一个结论:**并发和并行都是对“多任务”处理的描述,其中并发是轮流处理,而并行是同时处理**。
## CPU 多核
现在的个人计算机动辄拥有十来个核心M1 Max/Intel 12代如果使用串行的方式那真是太低调了因此我们把各种任务简单分成多个队列每个队列都交给一个 CPU 核心去执行,当某个 CPU 核心没有任务时,它还能去其它核心的队列中偷任务(真·老黄牛),这样就实现了并行化处理。
现在的个人计算机动辄拥有十来个核心M1 Max/Intel 12 代),如果使用串行的方式那真是太低调了,因此我们把各种任务简单分成多个队列,每个队列都交给一个 CPU 核心去执行,当某个 CPU 核心没有任务时,它还能去其它核心的队列中偷任务(真·老黄牛),这样就实现了并行化处理。
#### 单核心并发
那问题来了,在早期只有一个 CPU 核心时,我们的任务是怎么处理的呢?其实聪明的读者应该已经想到,是的,并发解君愁。当然,这里还得提到操作系统的多线程,正是操作系统多线程 + CPU 核心,才实现了现代化的多任务操作系统。
在 OS 级别,多线程负责管理我们的任务队列,你可以简单认为一个线程管理着一个任务队列,然后线程之间还能根据空闲度进行任务调度。我们的程序只会跟 OS 线程打交道,并不关心 CPU 到底有多少个核心,真正关心的只是 OS当线程把任务交给 CPU 核心去执行时,如果只有一个 CPU 核心,那么它就只能同时处理一个任务。
相信大家都看出来了:**CPU 核心**对应的是上图的咖啡机,而**多个线程的任务队列**就对应的多个排队的队列,由于终受限于 CPU 核心数,每个队列每次只会有一个任务被处理。
和排队一样,假如某个任务执行时间过长,就会导致用户界面的假死(相信使用 Windows 的同学或多或少都碰到过假死的问题), 那么就需要 CPU 的任务调度了(真实 CPU 的调度很复杂,我们这里做了简化),有一个调度器会按照某些条件从队列中选择任务进行执行,并且当一个任务执行时间过长时,会强行切换该任务到后台中(或者放入任务队列,真实情况很复杂!),去执行新的任务。
不断这样的快速任务切换,对用户而言就实现了表面上的多任务同时处理,但是实际上最终也只有一个 CPU 核心在不停的工作。
因此并发的关键在于:**快速轮换处理不同的任务**,给用户带来所有任务同时在运行的假象。
#### 多核心并行
当 CPU 核心增多到 `N` 时,那么同一时间就能有 `N` 个任务被处理,那么我们的并行度就是 `N`,相应的处理效率也变成了单核心的 `N` 倍(实际情况并没有这么高)。
#### 多核心并发
当核心增多到 `N` 时,操作系统同时在进行的任务肯定远不止 `N` 个,这些任务将被放入 `M` 个线程队列中,接着交给 `N` 个CPU核心去执行最后实现了 `M:N` 的处理模型,在这种情况下,**并发跟并行时同时在发生的,所有用户任务从表面来看都在并发的运行,其实实际上,同一时刻只有 `N` 个任务能被同时并行的处理**。
当核心增多到 `N` 时,操作系统同时在进行的任务肯定远不止 `N` 个,这些任务将被放入 `M` 个线程队列中,接着交给 `N` 个 CPU 核心去执行,最后实现了 `M:N` 的处理模型,在这种情况下,**并发跟并行时同时在发生的,所有用户任务从表面来看都在并发的运行,其实实际上,同一时刻只有 `N` 个任务能被同时并行的处理**。
看到这里,相信大家已经明白两者的区别,那么我们下面给出一个正式的定义(该定义摘选自<<并发的艺术>>)。
@ -52,8 +53,8 @@
相信你已经能够得出结论——**“并行”概念是“并发”概念的一个子集**。也就是说,你可以编写一个拥有多个线程或者进程的并发程序,但如果没有多核处理器来执行这个程序,那么就不能以并行方式来运行代码。因此,凡是在求解单个问题时涉及多个执行流程的编程模式或者执行行为,都属于并发编程的范畴。
## 编程语言的并发模型
如果大家学过其它语言的多线程,可能就知道不同语言对于线程的实现可能大相径庭:
- 由于操作系统提供了创建线程的 API因此部分语言会直接调用该 API 来创建线程,因此最终程序内的线程数和该程序占用的操作系统线程数相等,一般称之为**1:1 线程模型**,例如 Rust。
@ -67,10 +68,3 @@
而绿色线程/协程的实现会显著增大运行时的大小,因此 Rust 只在标准库中提供了 `1:1` 的线程模型,如果你愿意牺牲一些性能来换取更精确的线程控制以及更小的线程上下文切换成本,那么可以选择 Rust 中的 `M:N` 模型,这些模型由三方库提供了实现,例如大名鼎鼎的 `tokio`
在了解了并发和并行后,我们可以正式开始 Rust 的多线程之旅。

@ -1,4 +1,5 @@
# 多线程并发编程
安全和高效的处理并发是 Rust 语言的主要目标之一。随着现代处理器的核心数不断增加,并发和并行已经成为日常编程不可或缺的一部分,甚至于 Go 语言已经将并发简化到一个 `go` 关键字就可以。
可惜的是,在 Rust 中由于语言设计理念、安全、性能的多方面考虑,并没有采用 Go 语言大道至简的方式,而是选择了多线程与 `async/await` 相结合,优点是可控性更强、性能更高,缺点是复杂度并不低,当然这也是系统级语言的应有选择:**使用复杂度换取可控性和性能**。

@ -1,17 +1,21 @@
# 线程间的消息传递
在多线程间有多种方式可以共享、传递数据,最常用的方式就是通过消息传递或者将锁和`Arc`联合使用,而对于前者,在编程界还有一个大名鼎鼎的`Actor线程模型`为其背书典型的有Erlang语言还有Go语言中很经典的一句话
在多线程间有多种方式可以共享、传递数据,最常用的方式就是通过消息传递或者将锁和`Arc`联合使用,而对于前者,在编程界还有一个大名鼎鼎的`Actor线程模型`为其背书,典型的有 Erlang 语言,还有 Go 语言中很经典的一句话:
> Do not communicate by sharing memory; instead, share memory by communicating
而对于后者,我们将在下一节中进行讲述。
## 消息通道
与Go语言内置的`chan`不同Rust是在标准库里提供了消息通道(`channel`),你可以将其想象成一场直播,多个主播联合起来在搞一场直播,最终内容通过通道传输给屏幕前的我们,其中主播被称之为**发送者**,观众被称之为**接收者**,显而易见的是:一个通道应该支持多个发送者和接收者。
与 Go 语言内置的`chan`不同Rust 是在标准库里提供了消息通道(`channel`),你可以将其想象成一场直播,多个主播联合起来在搞一场直播,最终内容通过通道传输给屏幕前的我们,其中主播被称之为**发送者**,观众被称之为**接收者**,显而易见的是:一个通道应该支持多个发送者和接收者。
但是,在实际使用中,我们需要使用不同的库来满足诸如:**多发送者 -> 单接收者,多发送者 -> 多接收者**等场景形式,此时一个标准库显然就不够了,不过别急,让我们先从标准库讲起。
## 多发送者,单接收者
标准库提供了通道`std::sync::mpsc`,其中`mpsc`是*multiple producer, single consumer*的缩写,代表了该通道支持多个发送者,但是只支持唯一的接收者。 当然,支持多个发送者也意味着支持单个发送者,我们先来看看单发送者、单接收者的简单例子:
```rust
use std::sync::mpsc;
use std::thread;
@ -24,7 +28,7 @@ fn main() {
thread::spawn(move || {
// 发送一个数字1, send方法返回Result<T,E>通过unwrap进行快速错误处理
tx.send(1).unwrap();
// 下面代码将报错因为编译器自动推导出通道传递的值是i32类型那么Option<i32>类型将产生不匹配错误
// tx.send(Some(1)).unwrap()
});
@ -44,8 +48,10 @@ fn main() {
同样的,对于`recv`方法来说,当发送者关闭时,它也会接收到一个错误,用于说明不会再有任何值被发送过来。
## 不阻塞的try_recv方法
## 不阻塞的 try_recv 方法
除了上述`recv`方法,还可以使用`try_recv`尝试接收一次消息,该方法并**不会阻塞线程**,当通道中没有消息时,它会立刻返回一个错误:
```rust
use std::sync::mpsc;
use std::thread;
@ -62,11 +68,13 @@ fn main() {
```
由于子线程的创建需要时间,因此`println!`和`try_recv`方法会先执行,而此时子线程的**消息还未被发出**。`try_recv`会尝试立即读取一次消息,因为消息没有发出,此次读取最终会报错,且主线程运行结束(可悲的是,相对于主线程中的代码,子线程的创建速度实在是过慢,直到主线程结束,都无法完成子线程的初始化。。):
```console
receive Err(Empty)
```
如上,`try_recv`返回了一个错误,错误内容是`Empty`,代表通道并没有消息。如果你尝试把`println!`复制一些行,就会发现一个有趣的输出:
```console
···
receive Err(Empty)
@ -78,12 +86,14 @@ receive Err(Disconnected)
如上,当子线程创建成功且发送消息后,主线程会接收到`Ok(1)`的消息内容,紧接着子线程结束,发送者也随着被`drop`,此时接收者又会报错,但是这次错误原因有所不同:`Disconnected`代表发送者已经被关闭。
## 传输具有所有权的数据
使用通道来传输数据一样要遵循Rust的所有权规则
使用通道来传输数据,一样要遵循 Rust 的所有权规则:
- 若值的类型实现了`Copy`特征,则直接复制一份该值,然后传输过去,例如之前的`i32`类型
- 若值没有实现`Copy`,则它的所有权会被转移给接收端,在发送端继续使用该值将报错
一起来看看第二种情况:
```rust
use std::sync::mpsc;
use std::thread;
@ -103,6 +113,7 @@ fn main() {
```
以上代码中,`String`底层的字符串是存储在堆上,并没有实现`Copy`特征,当它被发送后,会将所有权从发送端的`s`转移给接收端的`received`,之后`s`将无法被使用:
```console
error[E0382]: borrow of moved value: `s`
--> src/main.rs:10:31
@ -115,10 +126,12 @@ error[E0382]: borrow of moved value: `s`
| ^ value borrowed here after move // 所有权被转移后依然对s进行了借用
```
各种细节不禁令人感叹Rust还是安全假如没有所有权的保护`String`字符串将被两个线程同时持有,任何一个线程对字符串内容的修改都会导致另外一个线程持有的字符串被改变,除非你故意这么设计,否则这就是不安全的隐患。
各种细节不禁令人感叹Rust 还是安全!假如没有所有权的保护,`String`字符串将被两个线程同时持有,任何一个线程对字符串内容的修改都会导致另外一个线程持有的字符串被改变,除非你故意这么设计,否则这就是不安全的隐患。
## 使用 for 进行循环接收
## 使用for进行循环接收
下面来看看如何连续接收通道中的值:
```rust
use std::sync::mpsc;
use std::thread;
@ -147,10 +160,12 @@ fn main() {
}
```
在上面代码中,主线程和子线程是并发运行的,子线程在不停的**发送消息 -> 休眠1秒**,与此同时,主线程使用`for`循环**阻塞**的从`rx`**迭代器**中接收消息,当子线程运行完成时,发送者`tx`会随之被`drop`,此时`for`循环将被终止,最终`main`线程成功结束。
在上面代码中,主线程和子线程是并发运行的,子线程在不停的**发送消息 -> 休眠 1 秒**,与此同时,主线程使用`for`循环**阻塞**的从`rx`**迭代器**中接收消息,当子线程运行完成时,发送者`tx`会随之被`drop`,此时`for`循环将被终止,最终`main`线程成功结束。
### 使用多发送者
由于子线程会拿走发送者的所有权,因此我们必须对发送者进行克隆,然后让每个线程拿走它的一份拷贝:
```rust
use std::sync::mpsc;
use std::thread;
@ -165,7 +180,7 @@ fn main() {
thread::spawn(move || {
tx1.send(String::from("hi from cloned tx")).unwrap();
});
for received in rx {
println!("Got: {}", received);
}
@ -181,16 +196,19 @@ fn main() {
- 由于两个子线程谁先创建完成是未知的,因此哪条消息先发送也是未知的,最终主线程的输出顺序也不确定
## 消息顺序
上述第三点的消息顺序仅仅是因为线程创建引起的,并不代表通道中的消息是无序的,对于通道而言,消息的发送顺序和接收顺序是一致的,满足`FIFO`原则(先进先出)。
由于篇幅有限,具体的代码这里就不再给出,感兴趣的读者可以自己验证下。
## 同步和异步通道
Rust标准库的`mpsc`通道其实分为两种类型:同步和异步。
Rust 标准库的`mpsc`通道其实分为两种类型:同步和异步。
#### 异步通道
之前我们使用的都是异步通道:无论接收者是否正在接收消息,消息发送者在发送消息时都不会阻塞:
```rust
use std::sync::mpsc;
use std::thread;
@ -214,6 +232,7 @@ fn main() {
```
运行后输出如下:
```console
睡眠之前
发送之前
@ -223,12 +242,14 @@ fn main() {
收到值 1
```
主线程因为睡眠阻塞了3秒因此并没有进行消息接收而子线程却在此期间轻松完成了消息的发送。等主线程睡眠结束后才姗姗来迟的从通道中接收了子线程老早之前发送的消息。
主线程因为睡眠阻塞了 3 秒,因此并没有进行消息接收,而子线程却在此期间轻松完成了消息的发送。等主线程睡眠结束后,才姗姗来迟的从通道中接收了子线程老早之前发送的消息。
从输出还可以看出,`发送之前`和`发送之后`是连续输出的,没有受到接收端主线程的任何影响,因此通过`mpsc::channel`创建的通道是异步通道。
#### 同步通道
与异步通道相反,同步通道**发送消息是阻塞的,只有在消息被接收后才解除阻塞**,例如:
```rust
use std::sync::mpsc;
use std::thread;
@ -252,6 +273,7 @@ fn main() {
```
运行后输出如下:
```console
睡眠之前
发送之前
@ -263,11 +285,12 @@ fn main() {
可以看出,主线程由于睡眠被阻塞导致无法接收消息,因此子线程的发送也一直被阻塞,直到主线程结束睡眠并成功接收消息后,发送才成功:**发送之后**的输出是在**收到值 1**之后,说明**只有接收消息彻底成功后,发送消息才算完成**。
#### 消息缓存
细心的读者可能已经发现在创建同步通道时,我们传递了一个参数`0`: `mpsc::sync_channel(0);`,这是什么意思呢?
答案不急给出,先将`0`改成`1`,然后再运行试试:
```console
睡眠之前
发送之前
@ -277,6 +300,7 @@ receive 1
```
纳尼。。竟然得到了和异步通道一样的效果:根本没有等待主线程的接收开始,消息发送就立即完成了! 难道同步通道变成了异步通道? 别急,将子线程中的代码修改下试试:
```rust
println!("首次发送之前");
tx.send(1).unwrap();
@ -286,6 +310,7 @@ println!("再次发送之后");
```
在子线程中,我们又多发了一条消息,此时输出如下:
```console
睡眠之前
首次发送之前
@ -304,15 +329,16 @@ Bingo更奇怪的事出现了第一条消息瞬间发送完成没有阻
因此,使用异步消息虽然能非常高效且不会造成发送线程的阻塞,但是存在消息未及时消费,最终内存过大的问题。在实际项目中,可以考虑使用一个带缓冲值的同步通道来避免这种风险。
## 关闭通道
之前我们数次提到了通道关闭,并且提到了当通道关闭后,发送消息或接收消息将会报错。那么如何关闭通道呢? 很简单:**所有发送者被`drop`或者所有接收者被`drop`后,通道会自动关闭**。
神奇的是这件事是在编译期实现的完全没有运行期性能损耗只能说Rust的`Drop`特征YYDS!
神奇的是,这件事是在编译期实现的,完全没有运行期性能损耗!只能说 Rust 的`Drop`特征 YYDS!
## 传输多种类型的数据
之前提到过,一个消息通道只能传输一种类型的数据,如果你想要传输多种类型的数据,可以为每个类型创建一个通道,你也可以使用枚举类型来实现:
```rust
use std::sync::mpsc::{self, Receiver, Sender};
@ -336,16 +362,18 @@ fn main() {
}
```
如上所示枚举类型还能让我们带上想要传输的数据但是有一点需要注意Rust会按照枚举中占用内存最大的那个成员进行内存对齐这意味着就算你传输的是枚举中占用内存最小的成员它占用的内存依然和最大的成员相同, 因此会造成内存上的浪费。
如上所示枚举类型还能让我们带上想要传输的数据但是有一点需要注意Rust 会按照枚举中占用内存最大的那个成员进行内存对齐,这意味着就算你传输的是枚举中占用内存最小的成员,它占用的内存依然和最大的成员相同, 因此会造成内存上的浪费。
## 新手容易遇到的坑
`mpsc`虽然相当简洁明了,但是在使用起来还是可能存在坑:
```rust
use std::sync::mpsc;
fn main() {
use std::thread;
let (send, recv) = mpsc::channel();
let num_threads = 3;
for i in 0..num_threads {
@ -355,15 +383,16 @@ fn main() {
println!("thread {:?} finished", i);
});
}
// 在这里drop send...
for x in recv {
println!("Got: {}", x);
}
println!("finished iterating");
}
```
以上代码看起来非常正常,但是运行后主线程会一直阻塞,最后一行打印输出也不会被执行,原因在于: 子线程拿走的是复制后的`send`的所有权,这些拷贝会在子线程结束后被`drop`,因此无需担心,但是`send`本身却直到`main`函数的结束才会被`drop`。
之前提到,通道关闭的两个条件:发送者全部`drop`或接收者被`drop`,要结束`for`循环显然是要求发送者全部`drop`,但是由于`send`自身没有被`drop`,会导致该循环永远无法结束,最终主线程会一直阻塞。
@ -371,7 +400,8 @@ fn main() {
解决办法很简单,`drop`掉`send`即可:在代码中的注释下面添加一行`drop(send);`。
## mpmc 更好的性能
如果你需要mpmc(多发送者,多接收者)或者需要更高的性能,可以考虑第三方库:
如果你需要 mpmc(多发送者,多接收者)或者需要更高的性能,可以考虑第三方库:
- [**crossbeam-channel**](https://github.com/crossbeam-rs/crossbeam/tree/master/crossbeam-channel), 老牌强库,功能较全,性能较强,之前是独立的库,但是后面合并到了`crossbeam`主仓库中
- [**flume**](https://github.com/zesterer/flume), 官方给出的性能数据某些场景要比crossbeam更好些
- [**flume**](https://github.com/zesterer/flume), 官方给出的性能数据某些场景要比 crossbeam 更好些

@ -1,8 +1,11 @@
# 基于Send和Sync的线程安全
为何Rc、RefCell和原生指针不可以在多线程间使用如何让原生指针可以在多线程使用我们一起来探寻下这些问题的答案。
# 基于 Send 和 Sync 的线程安全
为何 Rc、RefCell 和原生指针不可以在多线程间使用?如何让原生指针可以在多线程使用?我们一起来探寻下这些问题的答案。
## 无法用于多线程的`Rc`
先来看一段多线程使用`Rc`的代码:
```rust
use std::thread;
use std::rc::Rc;
@ -17,6 +20,7 @@ fn main() {
```
以上代码将`v`的所有权通过`move`转移到子线程中,看似正确实则会报错:
```console
error[E0277]: `Rc<i32>` cannot be sent between threads safely
------ 省略部分报错 --------
@ -25,8 +29,10 @@ error[E0277]: `Rc<i32>` cannot be sent between threads safely
表面原因是`Rc`无法在线程间安全的转移,实际是编译器给予我们的那句帮助: `the trait Send is not implemented for Rc<i32>`(`Rc<i32>`未实现`Send`特征), 那么此处的`Send`特征又是何方神圣?
## Rc和Arc源码对比
## Rc 和 Arc 源码对比
在介绍`Send`特征之前,再来看看`Arc`为何可以在多线程使用,玄机在于两者的源码实现上:
```rust
// Rc源码片段
impl<T: ?Sized> !marker::Send for Rc<T> {}
@ -39,17 +45,19 @@ unsafe impl<T: ?Sized + Sync + Send> Sync for Arc<T> {}
`!`代表移除特征的相应实现,上面代码中`Rc<T>`的`Send`和`Sync`特征被特地移除了实现,而`Arc<T>`则相反,实现了`Sync + Send`,再结合之前的编译器报错,大概可以明白了:`Send`和`Sync`是在线程间安全使用一个值的关键。
## Send和Sync
`Send`和`Sync`是Rust安全并发的重中之重但是实际上它们只是标记特征(marker trait该特征未定义任何行为因此非常适合用于标记), 来看看它们的作用:
## Send 和 Sync
`Send`和`Sync`是 Rust 安全并发的重中之重,但是实际上它们只是标记特征(marker trait该特征未定义任何行为因此非常适合用于标记), 来看看它们的作用:
- 实现`Send`的类型可以在线程间安全的传递其所有权
- 实现了`Sync`的类型可以在线程间安全的共享(通过引用)
这里还有一个潜在的依赖:一个类型要在线程间安全的共享的前提是,指向它的引用必须能在线程间传递。因为如果引用都不能被传递,我们就无法在多个线程间使用引用去访问同一个数据了。
由上可知,**若类型T的引用`&T`是`Send`,则`T`是`Sync`**。
由上可知,**若类型 T 的引用`&T`是`Send`,则`T`是`Sync`**。
没有例子的概念讲解都是耍流氓,来看看`RwLock`的实现:
```rust
unsafe impl<T: ?Sized + Send + Sync> Sync for RwLock<T> {}
```
@ -57,6 +65,7 @@ unsafe impl<T: ?Sized + Send + Sync> Sync for RwLock<T> {}
首先`RwLock`可以在线程间安全的共享,那它肯定是实现了`Sync`,但是我们的关注点不在这里。众多周知,`RwLock`可以并发的读,说明其中的值`T`必定也可以在线程间共享,那`T`必定要实现`Sync`。
果不其然,上述代码中,`T`的特征约束中就有一个`Sync`特征,那问题又来了,`Mutex`是不是相反?再来看看:
```rust
unsafe impl<T: ?Sized + Send> Sync for Mutex<T> {}
```
@ -66,9 +75,10 @@ unsafe impl<T: ?Sized + Send> Sync for Mutex<T> {}
武学秘籍再好,不见生死也是花拳绣腿。同样的,我们需要通过实战来彻底掌握`Send`和`Sync`,但在实战之前,先来简单看看有哪些类型实现了它们。
## 实现`Send`和`Sync`的类型
在Rust中几乎所有类型都默认实现了`Send`和`Sync`,而且由于这两个特征都是可自动派生的特征(通过`derive`派生),意味着一个复合类型(例如结构体), 只要它内部的所有成员都实现了`Send`或者`Sync`,那么它就自动实现了`Send`或`Sync`。
正是因为以上规则Rust中绝大多数类型都实现了`Send`和`Sync`,除了以下几个(事实上不止这几个,只不过它们比较常见):
在 Rust 中,几乎所有类型都默认实现了`Send`和`Sync`,而且由于这两个特征都是可自动派生的特征(通过`derive`派生),意味着一个复合类型(例如结构体), 只要它内部的所有成员都实现了`Send`或者`Sync`,那么它就自动实现了`Send`或`Sync`。
正是因为以上规则Rust 中绝大多数类型都实现了`Send`和`Sync`,除了以下几个(事实上不止这几个,只不过它们比较常见):
- 原生指针两者都没实现,因为它本身就没有任何安全保证
- `UnsafeCell`不是`Sync`,因此`Cell`和`RefCell`也不是
@ -81,7 +91,9 @@ unsafe impl<T: ?Sized + Send> Sync for Mutex<T> {}
至此,相关的概念大家已经掌握,但是我敢肯定,对于这两个滑不溜秋的家伙,大家依然会非常模糊,不知道它们该如何使用。那么我们来一起看看如何让原生指针可以在线程间安全的使用。
## 为原生指针实现`Send`
上面我们提到原生指针既没实现`Send`,意味着下面代码会报错:
```rust
use std::thread;
fn main() {
@ -93,9 +105,11 @@ fn main() {
t.join().unwrap();
}
```
报错跟之前无二: `*mut u8 cannot be sent between threads safely`, 但是有一个问题,我们无法为其直接实现`Send`特征,好在可以用[`newtype`类型](../custom-type.md#newtype) :`struct MyBox(*mut u8);`。
还记得之前的规则吗:复合类型中有一个成员没实现`Send`,该复合类型就不是`Send`,因此我们需要手动为它实现:
```rust
use std::thread;
@ -115,7 +129,9 @@ fn main() {
此时,我们的指针已经可以欢快的在多线程间撒欢,以上代码很简单,但有一点需要注意:`Send`和`Sync`是`unsafe`特征,实现时需要用`unsafe`代码块包裹。
## 为原生指针实现`Sync`
由于`Sync`是多线程间共享一个值,大家可能会想这么实现:
```rust
use std::thread;
fn main() {
@ -131,6 +147,7 @@ fn main() {
关于这种用法,在多线程章节也提到过,线程如果直接去借用其它线程的变量,会报错:`closure may outlive the current function,`, 原因在于编译器无法确定主线程`main`和子线程`t`谁的生命周期更长,特别是当两个线程都是子线程时,没有任何人知道哪个子线程会先结束,包括编译器!
因此我们得配合`Arc`去使用:
```rust
use std::thread;
use std::sync::Arc;
@ -152,6 +169,7 @@ fn main() {
```
上面代码将智能指针`v`的所有权转移给新线程,同时`v`包含了一个引用类型`b`,当在新的线程中试图获取内部的引用时,会报错:
```console
error[E0277]: `*const u8` cannot be shared between threads safely
--> src/main.rs:25:13
@ -163,18 +181,16 @@ error[E0277]: `*const u8` cannot be shared between threads safely
```
因为我们访问的引用实际上还是对主线程中的数据的借用,转移进来的仅仅是外层的智能指针引用。要解决很简单,为`MyBox`实现`Sync`:
```rust
unsafe impl Sync for MyBox {}
```
## 总结
通过上面的两个原生指针的例子,我们了解了如何实现`Send`和`Sync`,以及如何只实现`Send`而不实现`Sync`,简单总结下:
1. 实现`Send`的类型可以在线程间安全的传递其所有权, 实现`Sync`的类型可以在线程间安全的共享(通过引用)
2. 绝大部分类型都实现了`Send`和`Sync`常见的未实现的有原生指针、Cell/RefCell、Rc等
2. 绝大部分类型都实现了`Send`和`Sync`常见的未实现的有原生指针、Cell/RefCell、Rc
3. 可以为自定义类型实现`Send`和`Sync`,但是需要`unsafe`代码块
4. 可以为部分Rust中的类型实现`Send`、`Sync`,但是需要使用`newtype`,例如文中的原生指针例子
4. 可以为部分 Rust 中的类型实现`Send`、`Sync`,但是需要使用`newtype`,例如文中的原生指针例子

@ -1,9 +1,11 @@
# 线程同步锁、Condvar和信号量
# 线程同步锁、Condvar 和信号量
在多线程编程中,同步性极其的重要,当你需要同时访问一个资源、控制不同线程的执行次序时,都需要使用到同步性。
在Rust中有多种方式可以实现同步性。在上一节中讲到的消息传递就是同步性的一种实现方式例如我们可以通过消息传递来控制不同线程间的执行次序。还可以使用共享内存来实现同步性例如通过锁和原子操作等并发原语来实现多个线程同时且安全地去访问一个资源。
Rust 中有多种方式可以实现同步性。在上一节中讲到的消息传递就是同步性的一种实现方式,例如我们可以通过消息传递来控制不同线程间的执行次序。还可以使用共享内存来实现同步性,例如通过锁和原子操作等并发原语来实现多个线程同时且安全地去访问一个资源。
## 该如何选择
共享内存可以说是同步的灵魂,因为消息传递的底层实际上也是通过共享内存来实现,两者的区别如下:
- 共享内存相对消息传递能节省多次内存拷贝的成本
@ -18,15 +20,18 @@
而使用共享内存(并发原语)的场景往往就比较简单粗暴:需要简洁的实现以及更高的性能时。
总之,消息传递类似一个单所有权的系统:一个值同时只能有一个所有者,如果另一个线程需要该值的所有权,需要将所有权通过消息传递进行转移。而共享内存类似于一个多所有权的系统:多个线程可以同时访问同一个值。
总之,消息传递类似一个单所有权的系统:一个值同时只能有一个所有者,如果另一个线程需要该值的所有权,需要将所有权通过消息传递进行转移。而共享内存类似于一个多所有权的系统:多个线程可以同时访问同一个值。
## 互斥锁 Mutex
## 互斥锁Mutex
既然是共享内存,那并发原语自然是重中之重,先来一起看看皇冠上的明珠: 互斥锁`Mutex`(mutual exclusion的缩写)。
既然是共享内存,那并发原语自然是重中之重,先来一起看看皇冠上的明珠: 互斥锁`Mutex`(mutual exclusion 的缩写)。
`Mutex`让多个线程并发的访问同一个值变成了排队访问:同一时间,只允许一个线程`A`访问该值,其它线程需要等待`A`访问完成后才能继续。
#### 单线程中使用Mutex
先来看看单线程中`Mutex`该如何使用:
#### 单线程中使用 Mutex
先来看看单线程中`Mutex`该如何使用:
```rust
use std::sync::Mutex;
@ -57,10 +62,12 @@ fn main() {
正因为智能指针的使用,使得我们无需任何操作就能获取其中的数据。 如果释放锁,你需要做的仅仅是做好锁的作用域管理,例如上述代码的内部花括号使用,建议读者尝试下去掉内部的花括号,然后再次尝试获取第二个锁`num1`,看看会发生什么,友情提示:不会报错,但是主线程会永远阻塞,因为不幸发生了死锁。
#### 多线程中使用Mutex
#### 多线程中使用 Mutex
单线程中使用锁,说实话纯粹是为了演示功能,毕竟多线程才是锁的舞台。 现在,我们再来看看,如何在多线程下使用`Mutex`来访问同一个资源.
##### 无法运行的`Rc<T>`
```rust
use std::rc::Rc;
use std::sync::Mutex;
@ -94,15 +101,16 @@ fn main() {
由于子线程需要通过`move`拿走锁的所有权,因此我们需要使用多所有权来保证每个线程都拿到数据的独立所有权,恰好智能指针[`Rc<T>`](../smart-pointer/rc-arc.md)可以做到(**上面代码会报错**!具体往下看,别跳过-, -)。
以上代码实现了在多线程中计数的功能由于多个线程都需要去修改该计数器因此我们需要使用锁来保证同一时间只有一个线程可以修改计数器否则会导致脏数据想象一下A线程和B线程同时拿到计数器获取了当前值`1`, 并且同时对其进行了修改,最后值变成`2`,你会不会在风中凌乱?毕竟正确的值是`3`因为两个线程各自加1。
以上代码实现了在多线程中计数的功能,由于多个线程都需要去修改该计数器,因此我们需要使用锁来保证同一时间只有一个线程可以修改计数器,否则会导致脏数据:想象一下 A 线程和 B 线程同时拿到计数器,获取了当前值`1`, 并且同时对其进行了修改,最后值变成`2`,你会不会在风中凌乱?毕竟正确的值是`3`,因为两个线程各自加 1。
可能有人会说,有那么巧的事情吗?事实上,对于人类来说,因为干啥啥慢,并没有那么多巧合,所以人总会存在巧合心理。但是对于计算机而言,每秒可以轻松运行上亿次,在这种频次下,一切巧合几乎都将必然发生,因此千万不要有任何侥幸心理。
> 如果事情有变坏的可能,不管这种可能性有多小,它都会发生! - 在计算机领域歪打正着的墨菲定律
事实上,上面的代码会报错:
```console
error[E0277]: `Rc<Mutex<i32>>` cannot be sent between threads safely
error[E0277]: `Rc<Mutex<i32>>` cannot be sent between threads safely
// `Rc`无法在线程中安全的传输
--> src/main.rs:11:22
|
@ -123,8 +131,10 @@ error[E0277]: `Rc<Mutex<i32>>` cannot be sent between threads safely
错误中提到了一个关键点:`Rc<T>`无法在线程中传输,因为它没有实现`Send`特征(在下一节将详细介绍),而该特征可以确保数据在线程中安全的传输。
##### 多线程安全的Arc<T>
##### 多线程安全的 Arc<T>
好在,我们有`Arc<T>`,得益于它的[内部计数器](../smart-pointer/rc-arc.md#多线程无力的rc)是多线程安全的,因此可以在多线程环境中使用:
```rust
use std::sync::{Arc, Mutex};
use std::thread;
@ -152,35 +162,40 @@ fn main() {
```
以上代码可以顺利运行:
```console
Result: 10
```
#### 内部可变性
在之前章节,我们提到过[内部可变性](../smart-pointer/cell-refcell.md#内部可变性),其中`Rc<T>`和`RefCell<T>`的结合,可以实现单线程的内部可变性。
现在我们又有了新的武器,由于`Mutex<T>`可以支持修改内部数据,当结合`Arc<T>`一起使用时,可以实现多线程的内部可变性。
简单总结下:`Rc<T>/RefCell<T>`用于单线程内部可变性, `Arc<T>/Mutext<T>`用于多线程内部可变性。
#### 需要小心使用的 Mutex
#### 需要小心使用的Mutex
如果有其它语言的编程经验,就知道互斥锁这家伙不好对付,想要正确使用,你得牢记在心:
- 在使用数据前必须先获取锁
- 在数据使用完成后,必须**及时**的释放锁,比如文章开头的例子,使用内部语句块的目的就是为了及时的释放锁
这两点看起来不起眼但要正确的使用其实是相当不简单的对于其它语言忘记释放锁是经常发生的虽然Rust通过智能指针的`drop`机制帮助我们避免了这一点,但是由于不及时释放锁导致的性能问题也是常见的。
这两点看起来不起眼,但要正确的使用,其实是相当不简单的,对于其它语言,忘记释放锁是经常发生的,虽然 Rust 通过智能指针的`drop`机制帮助我们避免了这一点,但是由于不及时释放锁导致的性能问题也是常见的。
正因为这种困难性导致很多用户都热衷于使用消息传递的方式来实现同步例如Go语言直接把`channel`内置在语言特性中,甚至还有无锁的语言,例如`erlang`,完全使用`Actor`模型依赖消息传递来完成共享和同步。幸好Rust的类型系统、所有权机制、智能指针等可以很好的帮助我们减轻使用锁时的负担。
正因为这种困难性,导致很多用户都热衷于使用消息传递的方式来实现同步,例如 Go 语言直接把`channel`内置在语言特性中,甚至还有无锁的语言,例如`erlang`,完全使用`Actor`模型,依赖消息传递来完成共享和同步。幸好 Rust 的类型系统、所有权机制、智能指针等可以很好的帮助我们减轻使用锁时的负担。
另一个值的注意的是在使用`Mutex<T>`时Rust无法帮我们避免所有的逻辑错误例如在之前章节我们提到过使用`Rc<T>`可能会导致[循环引用的问题](../circle-self-ref/circle-reference.md)。类似的,`Mutex<T>`也存在使用上的风险,例如创建死锁(deadlock):当一个操作试图锁住两个资源,然后两个线程各自获取其中一个锁,并试图获取另一个锁时,就会造成死锁。
另一个值的注意的是在使用`Mutex<T>`时Rust 无法帮我们避免所有的逻辑错误,例如在之前章节,我们提到过使用`Rc<T>`可能会导致[循环引用的问题](../circle-self-ref/circle-reference.md)。类似的,`Mutex<T>`也存在使用上的风险,例如创建死锁(deadlock):当一个操作试图锁住两个资源,然后两个线程各自获取其中一个锁,并试图获取另一个锁时,就会造成死锁。
## 死锁
在Rust中有多种方式可以创建死锁了解这些方式有助于你提前规避可能的风险一起来看看。
在 Rust 中有多种方式可以创建死锁,了解这些方式有助于你提前规避可能的风险,一起来看看。
#### 单线程死锁
这种死锁比较容易规避,但是当代码复杂后还是有可能遇到:
```rust
use std::sync::Mutex;
@ -194,7 +209,9 @@ fn main() {
非常简单,只要你在另一个锁还未被释放时去申请新的锁,就会触发,当代码复杂后,这种情况可能就没有那么显眼。
#### 多线程死锁
当我们拥有两个锁,且两个线程各自使用了其中一个锁,然后试图去访问另一个锁时,就可能发生死锁:
```rust
use std::{sync::{Mutex, MutexGuard}, thread};
use std::thread::sleep;
@ -248,12 +265,14 @@ fn main() {
在上面的描述中,我们用了"可能"二字,原因在于死锁在这段代码中不是必然发生的,总有一次运行你能看到最后一行打印输出。这是由于子线程的初始化顺序和执行速度并不确定,我们无法确定哪个线程中的锁先被执行,因此也无法确定两个线程对锁的具体使用顺序。
但是可以简单的说明下死锁发生的必然条件线程1锁住了`mutex1`并且线程`2`锁住了`mutex2`然后线程1试图去访问`mutex2`,同时线程`2`试图去访问`mutex1`,就会死锁。 因为线程2需要等待线程1释放`mutex1`后,才会释放`mutex2`而与此同时线程1需要等待线程2释放`mutex2`后才能释放`mutex1`,这种情况造成了两个线程都无法释放对方需要的锁,最终死锁。
但是,可以简单的说明下死锁发生的必然条件:线程 1 锁住了`mutex1`并且线程`2`锁住了`mutex2`,然后线程 1 试图去访问`mutex2`,同时线程`2`试图去访问`mutex1`,就会死锁。 因为线程 2 需要等待线程 1 释放`mutex1`后,才会释放`mutex2`,而与此同时,线程 1 需要等待线程 2 释放`mutex2`后才能释放`mutex1`,这种情况造成了两个线程都无法释放对方需要的锁,最终死锁。
那么为何某些时候死锁不会发生原因很简单线程2在线程1锁`mutex1`之前就已经全部执行完了随之线程2的`mutex2`和`mutex1`被全部释放线程1对锁的获取将不再有竞争者。 同理线程1若全部被执行完那线程2也不会被锁因此我们在线程1中间加一个睡眠增加死锁发生的概率。如果你在线程2中同样的位置也增加一个睡眠那死锁将必然发生!
那么为何某些时候,死锁不会发生?原因很简单,线程 2 在线程 1 锁`mutex1`之前,就已经全部执行完了,随之线程 2 的`mutex2`和`mutex1`被全部释放,线程 1 对锁的获取将不再有竞争者。 同理,线程 1 若全部被执行完,那线程 2 也不会被锁,因此我们在线程 1 中间加一个睡眠,增加死锁发生的概率。如果你在线程 2 中同样的位置也增加一个睡眠,那死锁将必然发生!
#### try_lock
与`lock`方法不同,`try_lock`会**尝试**去获取一次锁,如果无法获取会返回一个错误,因此**不会发生阻塞**:
```rust
use std::{sync::{Mutex, MutexGuard}, thread};
use std::thread::sleep;
@ -308,6 +327,7 @@ fn main() {
```
为了演示`try_lock`的作用,我们特定使用了之前必定会死锁的代码,并且将`lock`替换成`try_lock`,与之前的结果不同,这段代码将不会再有死锁发生:
```console
线程 0 锁住了mutex1接着准备去锁mutex2 !
线程 1 锁住了mutex2, 准备去锁mutex1
@ -318,10 +338,12 @@ fn main() {
如上所示,当`try_lock`失败时,会报出一个错误:`Err("WouldBlock")`,接着线程中的剩余代码会继续执行,不会被阻塞。
> 一个有趣的命名规则在Rust标准库中使用`try_xxx`都会尝试进行一次操作,如果无法完成,就立即返回,不会发生阻塞。例如消息传递章节中的`try_recv`以及本章节中的`try_lock`
> 一个有趣的命名规则:在 Rust 标准库中,使用`try_xxx`都会尝试进行一次操作,如果无法完成,就立即返回,不会发生阻塞。例如消息传递章节中的`try_recv`以及本章节中的`try_lock`
## 读写锁 RwLock
## 读写锁RwLock
`Mutex`会对每次读写都进行加锁,但某些时候,我们需要大量的并发读,`Mutex`就无法满足需求了,此时就可以使用`RwLock`:
```rust
use std::sync::RwLock;
@ -341,7 +363,7 @@ fn main() {
let mut w = lock.write().unwrap();
*w += 1;
assert_eq!(*w, 6);
// 以下代码会panic因为读和写不允许同时存在
// 写锁w直到该语句块结束才被释放因此下面的读锁依然处于`w`的作用域中
// let r1 = lock.read();
@ -351,12 +373,14 @@ fn main() {
```
`RwLock`在使用上和`Mutex`区别不大,需要注意的是,当读写同时发生时,程序会直接`panic`(本例是单线程,实际上多个线程中也是如此),因为会发生死锁:
```console
thread 'main' panicked at 'rwlock read lock would result in deadlock', /rustc/efec545293b9263be9edfb283a7aa66350b3acbf/library/std/src/sys/unix/rwlock.rs:49:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```
好在我们可以使用`try_write`和`try_read`来尝试进行一次写/读,若失败则返回错误:
```console
Err("WouldBlock")
```
@ -366,13 +390,14 @@ Err("WouldBlock")
1. 同时允许多个读,但最多只能有一个写
2. 读和写不能同时存在
3. 读可以使用`read`、`try_read`,写`write`、`try_write`, 在实际项目中,`try_xxx`会安全的多
## Mutex还是RwLock
## Mutex 还是 RwLock
首先简单性上`Mutex`完胜,因为使用`RwLock`你得操心几个问题:
- 读和写不能同时发生,如果使用`try_xxx`解决,就必须做大量的错误处理和失败重试机制
- 当读多写少时,写操作可能会因为一直无法获得锁导致连续多次失败([writer starvation](https://stackoverflow.com/questions/2190090/how-to-prevent-writer-starvation-in-a-read-write-lock-in-pthreads))
- RwLock其实是操作系统提供的实现原理要比`Mutex`复杂的多,因此单就锁的性能而言,比不上原生实现的`Mutex`
- RwLock 其实是操作系统提供的,实现原理要比`Mutex`复杂的多,因此单就锁的性能而言,比不上原生实现的`Mutex`
再来简单总结下两者的使用场景:
@ -386,19 +411,20 @@ Err("WouldBlock")
总之,如果你要使用`RwLock`要确保满足以下两个条件:**并发读,且需要对读到的资源进行"长时间"的操作**`HashMap`也许满足了并发读的需求,但是往往并不能满足后者:"长时间"的操作。
> benchmark永远是你在迷茫时最好的朋友
> benchmark 永远是你在迷茫时最好的朋友!
## 三方库提供的锁实现
标准库在设计时总会存在取舍,因为往往性能并不是最好的,如果你追求性能,可以使用三方库提供的并发原语:
- [parking_lot](https://crates.io/crates/parking_lot), 功能更完善、稳定社区较为活跃star较多更新较为活跃
- [parking_lot](https://crates.io/crates/parking_lot), 功能更完善、稳定社区较为活跃star 较多,更新较为活跃
- [spin](https://crates.io/crates/spin), 在多数场景中性能比`parking_lot`高一点,最近没怎么更新
如果不是追求特别极致的性能,建议选择前者。
## 用条件变量(Condvar)控制线程的同步
`Mutex`用于解决资源安全访问的问题但是我们还需要一个手段来解决资源访问顺序的问题。而Rust考虑到了这一点为我们提供了条件变量(Condition Variables),它经常和`Mutex`一起使用,可以让线程挂起,直到某个条件发生后再继续执行,其实`Condvar`我们在之前的多线程章节就已经见到过,现在再来看一个不同的例子:
`Mutex`用于解决资源安全访问的问题,但是我们还需要一个手段来解决资源访问顺序的问题。而 Rust 考虑到了这一点,为我们提供了条件变量(Condition Variables),它经常和`Mutex`一起使用,可以让线程挂起,直到某个条件发生后再继续执行,其实`Condvar`我们在之前的多线程章节就已经见到过,现在再来看一个不同的例子:
```rust
use std::sync::{Arc,Mutex,Condvar};
@ -447,6 +473,7 @@ fn main() {
```
例子中通过主线程来触发子线程实现交替打印输出:
```console
outside counter: 1
inner counter: 1
@ -457,12 +484,11 @@ inner counter: 3
Mutex { data: true, poisoned: false, .. }
```
## 信号量 Semaphore
## 信号量Semaphore
在多线程中,另一个重要的概念就是信号量,使用它可以让我们精准的控制当前正在运行的任务最大数量。想象一下,当一个新游戏刚开服时(有些较火的老游戏也会,比如`wow`),往往会控制游戏内玩家的同时在线数,一旦超过某个临界值,就开始进行排队进服。而在实际使用中,也有很多时候,我们需要通过信号量来控制最大并发数,防止服务器资源被撑爆。
本来Rust在标准库中有提供一个[信号量实现](https://doc.rust-lang.org/1.8.0/std/sync/struct.Semaphore.html), 但是由于各种原因这个库现在已经不再推荐使用了,因此我们推荐使用`tokio`中提供的`Semaphone`实现: [`tokio::sync::Semaphore`](https://github.com/tokio-rs/tokio/blob/master/tokio/src/sync/semaphore.rs)。
本来 Rust 在标准库中有提供一个[信号量实现](https://doc.rust-lang.org/1.8.0/std/sync/struct.Semaphore.html), 但是由于各种原因这个库现在已经不再推荐使用了,因此我们推荐使用`tokio`中提供的`Semaphone`实现: [`tokio::sync::Semaphore`](https://github.com/tokio-rs/tokio/blob/master/tokio/src/sync/semaphore.rs)。
```rust
use std::sync::Arc;
@ -489,13 +515,15 @@ async fn main() {
}
```
上面代码创建了一个容量为3的信号量当正在执行的任务超过3时剩下的任务需要等待正在执行任务完成并减少信号量后到3以内时才能继续执行。
上面代码创建了一个容量为 3 的信号量,当正在执行的任务超过 3 时,剩下的任务需要等待正在执行任务完成并减少信号量后到 3 以内时,才能继续执行。
这里的关键其实说白了就在于:信号量的申请和归还,使用前需要申请信号量,如果容量满了,就需要等待;使用后需要释放信号量,以便其它等待者可以继续。
## 总结
在很多时候,消息传递都是非常好用的手段,它可以让我们的数据在任务流水线上不断流转,实现起来非常优雅。
但是它并不能优雅的解决所有问题,因为我们面临的真实世界是非常复杂的,无法用某一种银弹统一解决。当面临消息传递不太适用的场景时,或者需要更好的性能和简洁性时,我们往往需要用锁来解决这些问题,因为锁允许多个线程同时访问同一个资源,简单粗暴。
除了锁之外其实还有一种并发原语可以帮助我们解决并发访问数据的问题那就是原子类型Atomic在下一章节中我们会对其进行深入讲解。
除了锁之外,其实还有一种并发原语可以帮助我们解决并发访问数据的问题,那就是原子类型 Atomic在下一章节中我们会对其进行深入讲解。

@ -1,17 +1,19 @@
# 线程同步Atomic原子类型与内存顺序
# 线程同步Atomic 原子类型与内存顺序
`Mutex`用起来简单,但是无法并发读,`RwLock`可以并发读,但是使用场景较为受限且性能不够,那么有没有一种全能性选手呢? 欢迎我们的`Atomic`闪亮登场。
从Rust1.34版本后就正式支持原子类型。原子指的是一系列不可被CPU上下文交换的机器指令这些指令组合在一起就形成了原子操作。在多核CPU下当某个CPU核心开始运行原子操作时会先暂停其它CPU内核对内存的操作以保证原子操作不会被其它CPU内核所干扰。
Rust1.34 版本后,就正式支持原子类型。原子指的是一系列不可被 CPU 上下文交换的机器指令,这些指令组合在一起就形成了原子操作。在多核 CPU 下,当某个 CPU 核心开始运行原子操作时,会先暂停其它 CPU 内核对内存的操作,以保证原子操作不会被其它 CPU 内核所干扰。
由于原子操作是通过指令提供的支持,因此它的性能相比锁和消息传递会好很多。相比较于锁而言,原子类型不需要开发者处理加锁和释放锁的问题,同时支持修改,读取等操作,还具备较高的并发性能,几乎所有的语言都支持原子类型。
可以看出原子类型是无锁类型,但是无锁不代表无需等待,因为原子类型内部使用了`CAS`循环,当大量的冲突发生时,该等待还是得[等待](./thread.md#多线程的开销)!但是总归比锁要好。
> CAS全称是Compare and swap, 它通过一条指令读取指定的内存地址,然后判断其中的值是否等于给定的前置值,如果相等,则将其修改为新的值
> CAS 全称是 Compare and swap, 它通过一条指令读取指定的内存地址,然后判断其中的值是否等于给定的前置值,如果相等,则将其修改为新的值
## 使用 Atomic 作为全局变量
## 使用Atomic作为全局变量
原子类型的一个常用场景,就是作为全局变量来使用:
```rust
use std::ops::Sub;
use std::sync::atomic::{AtomicU64, Ordering};
@ -49,34 +51,36 @@ fn main() {
}
```
以上代码启动了数个线程每个线程都在疯狂对全局变量进行加1操作, 最后将它与`线程数 * 加1次数`进行比较如果发生了因为多个线程同时修改导致了脏数据那么这两个必将不相等。好在它没有让我们失望不仅快速的完成了任务而且保证了100%的并发安全性。
以上代码启动了数个线程,每个线程都在疯狂对全局变量进行加 1 操作, 最后将它与`线程数 * 加1次数`进行比较,如果发生了因为多个线程同时修改导致了脏数据,那么这两个必将不相等。好在,它没有让我们失望,不仅快速的完成了任务,而且保证了 100%的并发安全性。
当然以上代码的功能其实也可以通过`Mutex`来实现,但是后者的强大功能是建立在额外的性能损耗基础上的,因此性能会逊色不少:
```console
Atomic实现673ms
Mutex实现: 1136ms
```
可以看到`Atomic`实现会比`Mutex`快**41%**,实际上在复杂场景下还能更快(甚至达到4倍的性能差距)
可以看到`Atomic`实现会比`Mutex`快**41%**,实际上在复杂场景下还能更快(甚至达到 4 倍的性能差距)
还有一点值得注意: **和`Mutex`一样,`Atomic`的值具有内部可变性**,你无需将其声明为`mut`
```rust
use std::sync::Mutex;
use std::sync::atomic::{Ordering, AtomicU64};
struct Counter {
count: u64
}
fn main() {
let n = Mutex::new(Counter {
count: 0
});
n.lock().unwrap().count += 1;
let n = AtomicU64::new(0);
n.fetch_add(0, Ordering::Relaxed);
}
```
@ -84,14 +88,17 @@ fn main() {
这里有一个奇怪的枚举成员`Ordering::Relaxed`, 看上去很像是排序作用,但是我们并没有做排序操作啊?实际上它用于控制原子操作使用的**内存顺序**。
## 内存顺序
内存顺序是指CPU在访问内存时的顺序该顺序可能受以下因素的影响
内存顺序是指 CPU 在访问内存时的顺序,该顺序可能受以下因素的影响:
- 代码中的先后顺序
- 编译器优化导致在编译阶段发生改变(内存重排序reordering)
- 运行阶段因CPU的缓存机制导致顺序被打乱
- 编译器优化导致在编译阶段发生改变(内存重排序 reordering)
- 运行阶段因 CPU 的缓存机制导致顺序被打乱
#### 编译器优化导致内存顺序的改变
对于第二点,我们举个例子:
```rust
static mut X: u64 = 0;
static mut Y: u64 = 1;
@ -112,9 +119,10 @@ fn main() {
```
假如在`C`和`D`代码片段中,根本没有用到`X = 1`,那么编译器很可能会将`X = 1`和`X = 2`进行合并:
```rust
... // A
unsafe {
... // B
X = 2;
@ -127,11 +135,13 @@ unsafe {
若代码`A`中创建了一个新的线程用于读取全局静态变量`X`,则该线程将无法读取到`X = 1`的结果,因为在编译阶段就已经被优化掉。
#### CPU缓存导致的内存顺序的改变
#### CPU 缓存导致的内存顺序的改变
假设之前的`X = 1`没有被优化掉,并且在代码片段`A`中有一个新的线程:
```console
initial state: X = 0, Y = 1
THREAD Main THREAD A
X = 1; if X == 1 {
Y = 3; Y *= 2;
@ -141,26 +151,28 @@ X = 2; }
我们来讨论下以上线程状态,`Y`最终的可能值(可能性依次降低):
- `Y = 3`: 线程`Main`运行完后才运行线程`A`,或者线程`A`运行完后再运行线程`Main`
- `Y = 6`: 线程`Main`的`Y = 3`运行完,但`X = 2`还没被运行, 此时线程A开始运行`Y *= 2`, 最后才运行`Main`线程的`X = 2`
- `Y = 2`: 线程`Main`正在运行`Y = 3`还没结束,此时线程`A`正在运行`Y *= 2`, 因此`Y`取到了值1然后`Main`的线程将`Y`设置为3 紧接着就被线程`A`的`Y = 2`所覆盖
- `Y = 2`: 上面的还只是一般的数据竞争,这里虽然产生了相同的结果`2`,但是背后的原理大相径庭: 线程`Main`运行完`Y = 3`但是CPU缓存中的`Y = 3`还没有被同步到其它CPU缓存中此时线程`A`中的`Y *= 2`就开始读取`Y`,结果读到了值`1`,最终计算出结果`2`
- `Y = 6`: 线程`Main`的`Y = 3`运行完,但`X = 2`还没被运行, 此时线程 A 开始运行`Y *= 2`, 最后才运行`Main`线程的`X = 2`
- `Y = 2`: 线程`Main`正在运行`Y = 3`还没结束,此时线程`A`正在运行`Y *= 2`, 因此`Y`取到了值 1然后`Main`的线程将`Y`设置为 3 紧接着就被线程`A`的`Y = 2`所覆盖
- `Y = 2`: 上面的还只是一般的数据竞争,这里虽然产生了相同的结果`2`,但是背后的原理大相径庭: 线程`Main`运行完`Y = 3`,但是 CPU 缓存中的`Y = 3`还没有被同步到其它 CPU 缓存中,此时线程`A`中的`Y *= 2`就开始读取`Y`,结果读到了值`1`,最终计算出结果`2`
甚至更改成:
```console
initial state: X = 0, Y = 1
THREAD Main THREAD A
X = 1; if X == 2 {
Y = 3; Y *= 2;
X = 2; }
```
还是可能出现`Y=2`,因为`Main`线程中的`X`和`Y`被同步到其它CPU缓存中的顺序未必一致。
还是可能出现`Y=2`,因为`Main`线程中的`X`和`Y`被同步到其它 CPU 缓存中的顺序未必一致。
#### 限定内存顺序的 5 个规则
#### 限定内存顺序的5个规则
在理解了内存顺序可能存在的改变后你就可以明白为什么Rust提供了`Ordering::Relaxed`用于限定内存顺序了事实上该枚举有5个成员:
在理解了内存顺序可能存在的改变后,你就可以明白为什么 Rust 提供了`Ordering::Relaxed`用于限定内存顺序了,事实上,该枚举有 5 个成员:
- **Relaxed** 这是最宽松的规则它对编译器和CPU不做任何限制可以乱序
- **Relaxed** 这是最宽松的规则,它对编译器和 CPU 不做任何限制,可以乱序
- **Release 释放**,设定内存屏障(Memory barrier),保证它之前的操作永远在它之前,但是它后面的操作可能被重排到它前面
- **Acquire 获取**, 设定内存屏障,保证在它之后的访问永远在它之后,但是它之前的操作却有可能被重排到它后面,往往和`Release`在不同线程中联合使用
- **AcqRel**, **Acquire**和**Release**的结合,同时拥有它们俩提供的保证。比如你要对一个 `atomic` 自增 1同时希望该操作之前和之后的读取或写入操作不会被重新排序
@ -169,21 +181,23 @@ X = 2; }
这些规则由于是系统提供的,因此其它语言提供的相应规则也大同小异,大家如果不明白可以看看其它语言的相关解释。
#### 内存屏障的例子
下面我们以`Release`和`Acquire`为例使用它们构筑出一对内存屏障防止编译器和CPU将屏障前(Release)和屏障后(Acquire)中的数据操作重新排在屏障围成的范围之外:
下面我们以`Release`和`Acquire`为例,使用它们构筑出一对内存屏障,防止编译器和 CPU 将屏障前(Release)和屏障后(Acquire)中的数据操作重新排在屏障围成的范围之外:
```rust
use std::thread::{self, JoinHandle};
use std::sync::atomic::{Ordering, AtomicBool};
static mut DATA: u64 = 0;
static READY: AtomicBool = AtomicBool::new(false);
fn reset() {
unsafe {
DATA = 0;
}
READY.store(false, Ordering::Relaxed);
}
fn producer() -> JoinHandle<()> {
thread::spawn(move || {
unsafe {
@ -192,40 +206,41 @@ fn producer() -> JoinHandle<()> {
READY.store(true, Ordering::Release); // B: 内存屏障 ↑
})
}
fn consumer() -> JoinHandle<()> {
thread::spawn(move || {
while !READY.load(Ordering::Acquire) {} // C: 内存屏障 ↓
assert_eq!(100, unsafe { DATA }); // D
})
}
fn main() {
loop {
reset();
let t_producer = producer();
let t_consumer = consumer();
t_producer.join().unwrap();
t_consumer.join().unwrap();
}
}
```
原则上,`Acquire`用于读取,而`Release`用于写入。但是由于有些原子操作同时拥有读取和写入的功能,此时就需要使用`AcqRel`来设置内存顺序了。在内存屏障中被写入的数据都可以被其它线程读取到不会有CPU缓存的问题。
原则上,`Acquire`用于读取,而`Release`用于写入。但是由于有些原子操作同时拥有读取和写入的功能,此时就需要使用`AcqRel`来设置内存顺序了。在内存屏障中被写入的数据,都可以被其它线程读取到,不会有 CPU 缓存的问题。
**内存顺序的选择**
1. 不知道怎么选择时,优先使用`SeqCst`,虽然会稍微减慢速度,但是慢一点也比出现错误好
1. 不知道怎么选择时,优先使用`SeqCst`,虽然会稍微减慢速度,但是慢一点也比出现错误好
2. 多线程只计数`fetch_add`而不使用该值触发其他逻辑分支的简单使用场景,可以使用`Relaxed`
参考 [Which std::sync::atomic::Ordering to use?](https://stackoverflow.com/questions/30407121/which-stdsyncatomicordering-to-use)
参考 [Which std::sync::atomic::Ordering to use?](https://stackoverflow.com/questions/30407121/which-stdsyncatomicordering-to-use)
## 多线程中使用Atomic
## 多线程中使用 Atomic
在多线程环境中要使用`Atomic`需要配合`Arc`
```rust
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
@ -250,20 +265,20 @@ fn main() {
}
```
## Atomic 能替代锁吗
## Atomic能替代锁吗
那么原子类型既然这么全能,它可以替代锁吗?答案是不行:
- 对于复杂的场景下,锁的使用简单粗暴,不容易有坑
- `std::sync::atomic`包中仅提供了数值类型的原子操作:`AtomicBool`, `AtomicIsize`, `AtomicUsize`, `AtomicI8`, `AtomicU16`等,而锁可以应用于各种类型
- 在有些情况下,必须使用锁来配合,例如上一章节中使用`Mutex`配合`Condvar`
## Atomic 的应用场景
## Atomic的应用场景
事实上,`Atomic`虽然对于用户不太常用,但是对于高性能库的开发者、标准库开发者都非常常用,它是并发原语的基石,除此之外,还有一些场景适用:
- 无锁(lock free)数据结构
- 全局变量例如全局自增ID, 在后续章节会介绍
- 全局变量,例如全局自增 ID, 在后续章节会介绍
- 跨线程计数器,例如可以用于统计指标
以上列出的只是`Atomic`适用的部分场景,具体场景需要大家未来根据自己的需求进行权衡选择。

@ -1,7 +1,9 @@
# 使用线程
放在十年前多线程编程可能还是一个少数人才掌握的核心概念但是在今天随着编程语言的不断发展多线程、多协程、Actor 等并发编程方式已经深入人心,同时多线程编程的门槛也在不断降低,本章节我们来看看在 Rust 中该如何使用多线程。
## 多线程编程的风险
由于多线程的代码是同时运行的,因此我们无法保证线程间的执行顺序,这会导致一些问题:
- 竞态条件(race conditions),多个线程以非一致性的顺序同时访问数据资源
@ -11,7 +13,9 @@
虽然 Rust 已经通过各种机制减少了上述情况的发生,但是依然无法完全避免上述情况,因此我们在编程时需要格外的小心,同时本书也会列出多线程编程时常见的陷阱,让你提前规避可能的风险。
## 创建线程
使用 `thread::spawn` 可以创建线程:
```rust
use std::thread;
use std::time::Duration;
@ -32,11 +36,13 @@ fn main() {
```
有几点值得注意:
- 线程内部的代码使用闭包来执行
- `main` 线程一旦结束,程序就立刻结束,因此需要保持它的存活,直到其它子线程完成自己的任务
- `thread::sleep` 会让当前线程休眠指定的时间,随后其它线程会被调度运行(上一节并发与并行中有简单介绍过),因此就算你的电脑只有一个 CPU 核心,该程序也会表现的如同多 CPU 核心一般,这就是并发!
来看看输出:
```console
hi number 1 from the main thread!
hi number 1 from the spawned thread!
@ -52,9 +58,11 @@ hi number 5 from the spawned thread!
如果多运行几次,你会发现好像每次输出会不太一样,因为:虽说线程往往是轮流执行的,但是这一点无法被保证!线程调度的方式往往取决于你使用的操作系统。总之,**千万不要依赖线程的执行顺序**。
## 等待子线程的结束
上面的代码你不但可能无法让子线程从 1 顺序打印到 10而且可能打印的数字会变少因为主线程会提前结束导致子线程也随之结束更过分的是如果当前系统繁忙甚至该子线程还没被创建主线程就已经结束了
因此我们需要一个方法,让主线程安全、可靠地等所有子线程完成任务后,再 kill self
```rust
use std::thread;
use std::time::Duration;
@ -77,6 +85,7 @@ fn main() {
```
通过调用 `handle.join`,可以让当前线程阻塞,直到它等待的子线程的结束,在上面代码中,由于 `main` 线程会被阻塞,因此它直到子线程结束后才会输出自己的 `1..5`
```console
hi number 1 from the spawned thread!
hi number 2 from the spawned thread!
@ -91,9 +100,11 @@ hi number 4 from the main thread!
以上输出清晰的展示了线程阻塞的作用,如果你将 `handle.join` 放置在 `main` 线程中的 `for` 循环后面,那就是另外一个结果:两个线程交替输出。
## 在线程闭包中使用 move
在[闭包](https://course.rs/advance/functional-programing/closure.html#move-和-fn)章节中,有讲过 `move` 关键字在闭包中的使用可以让该闭包拿走环境中某个值的所有权,同样地,你可以使用 `move` 来将所有权从一个线程转移到另外一个线程。
首先,来看看在一个线程中直接使用另一个线程中的数据会如何:
```rust
use std::thread;
@ -109,6 +120,7 @@ fn main() {
```
以上代码在子线程的闭包中捕获了环境中的 `v` 变量,来看看结果:
```console
error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
--> src/main.rs:6:32
@ -133,6 +145,7 @@ help: to force the closure to take ownership of `v` (and any other referenced va
```
其实代码本身并没有什么问题,问题在于 Rust 无法确定新的线程会活多久(多个线程的结束顺序并不是固定的),所以也无法确定新线程所引用的 `v` 是否在使用过程中一直合法:
```rust
use std::thread;
@ -148,9 +161,11 @@ fn main() {
handle.join().unwrap();
}
```
大家要记住,线程的启动时间点和结束时间点是不确定的,因此存在一种可能,当主线程执行完, `v` 被释放掉时,新的线程很可能还没有结束甚至还没有被创建成功,此时新线程对 `v` 的引用立刻就不再合法!
好在报错里进行了提示:`to force the closure to take ownership of v (and any other referenced variables), use the move keyword`,让我们使用 `move` 关键字拿走 `v` 的所有权即可:
```rust
use std::thread;
@ -171,16 +186,18 @@ fn main() {
如上所示,很简单的代码,而且 Rust 的所有权机制保证了数据使用上的安全:`v` 的所有权被转移给新的线程后,`main` 线程将无法继续使用:最后一行代码将报错。
## 线程是如何结束的
之前我们提到 `main` 线程是程序的主线程,一旦结束,则程序随之结束,同时各个子线程也将被强行终止。那么有一个问题,如果父线程不是 `main` 线程,那么父线程的结束会导致什么?自生自灭还是被干掉?
在系统编程中,操作系统提供了直接杀死线程的接口,简单粗暴,但是 Rust 并没有提供这样的接口,原因在于,粗暴地终止一个线程可能会导致资源没有释放、状态混乱等不可预期的结果,一向以安全自称的 Rust自然不会砸自己的饭碗。
那么 Rust 中线程是如何结束的呢?答案很简单:线程的代码执行完,线程就会自动结束。但是如果线程中的代码不会执行完呢?那么情况可以分为两种进行讨论:
- 线程的任务是一个循环 IO 读取任务流程类似IO 阻塞,等待读取新的数据 -> 读到数据,处理完成 -> 继续阻塞等待 ··· -> 收到socket关闭的信号 -> 结束线程在此过程中绝大部分时间线程都处于阻塞的状态因此虽然看上去是循环CPU 占用其实很小,也是网络服务中最最常见的模型
- 线程的任务是一个循环 IO 读取任务流程类似IO 阻塞,等待读取新的数据 -> 读到数据,处理完成 -> 继续阻塞等待 ··· -> 收到 socket 关闭的信号 -> 结束线程在此过程中绝大部分时间线程都处于阻塞的状态因此虽然看上去是循环CPU 占用其实很小,也是网络服务中最最常见的模型
- 线程的任务是一个循环,里面没有任何阻塞,包括休眠这种操作也没有,此时 CPU 很不幸的会被跑满,而且你如果没有设置终止条件,该线程将持续跑满一个 CPU 核心,并且不会被终止,直到 `main` 线程的结束
第一情况很常见,我们来模拟看看第二种情况:
第一情况很常见,我们来模拟看看第二种情况:
```rust
use std::thread;
use std::time::Duration;
@ -208,20 +225,24 @@ fn main() {
从之前的线程结束规则,我们可以猜测程序将这样执行:`A` 线程结束后,由它创建的 `B` 线程仍在疯狂输出,直到 `main` 线程在 100 毫秒后结束。如果你把该时间增加到几十秒,就可以看到你的 CPU 核心 100% 的盛况了-,-
## 多线程的性能
下面我们从多个方面来看看多线程的性能大概是怎么样的。
#### 创建线程的性能
据不精确估算,创建一个线程大概需要 0.24 毫秒,随着线程的变多,这个值会变得更大,因此线程的创建耗时并不是不可忽略的,只有当真的需要处理一个值得用线程去处理的任务时,才使用线程,一些鸡毛蒜皮的任务,就无需创建线程了。
#### 创建多少线程合适
因为 CPU 的核心数限制,当任务是 CPU 密集型时,就算线程数超过了 CPU 核心数,也并不能帮你获得更好的性能,因为每个线程的任务都可以轻松让 CPU 的某个核心跑满,既然如此,让线程数等于 CPU 核心数是最好的。
但是当你的任务大部分时间都处于阻塞状态时,就可以考虑增多线程数量,这样当某个线程处于阻塞状态时,会被切走,进而运行其它的线程,典型就是网络 IO 操作,我们可以为每一个进来的用户连接创建一个线程去处理,该连接绝大部分时间都是处于 IO 读取阻塞状态,因此有限的 CPU 核心完全可以处理成百上千的用户连接线程,但是事实上,对于这种网络 IO 情况,一般都不再使用多线程的方式了,毕竟操作系统的线程数是有限的,意味着并发数也很容易达到上限,而且过多的线程也会导致线程上下文切换的代价过大,使用 `async/await``M:N` 并发模型,就没有这个烦恼。
#### 多线程的开销
下面的代码是一个无锁实现(CAS)的 `Hashmap` 在多线程下的使用:
```rust
for i in 0..num_threads {
let ht = Arc::clone(&ht);
@ -260,7 +281,9 @@ for handle in handles {
总之,多线程的开销往往是在锁、数据竞争、缓存失效上,这些限制了现代化软件系统随着 CPU 核心的增多性能也线性增加的野心。
## 线程屏障(Barrier)
在 Rust 中,可以使用 `Barrier` 让多个线程都执行到某个点后,才继续一起往后执行:
```rust
use std::sync::{Arc, Barrier};
use std::thread;
@ -285,6 +308,7 @@ fn main() {
```
上面代码,我们在线程打印出 `before wait` 后增加了一个屏障,目的就是等所有的线程都打印出**before wait**后,各个线程再继续执行:
```console
before wait
before wait
@ -301,10 +325,13 @@ after wait
```
## 线程局部变量(Thread Local Variable)
对于多线程编程,线程局部变量在一些场景下非常有用,而 Rust 通过标准库和三方库对此进行了支持。
#### 标准库 thread_local
使用 `thread_local` 宏可以初始化线程局部变量,然后在线程内部使用该变量的 `with` 方法获取变量值:
```rust
use std::cell::RefCell;
use std::thread;
@ -338,6 +365,7 @@ FOO.with(|f| {
可以注意到,线程中对 `FOO` 的使用是通过借用的方式,但是若我们需要每个线程独自获取它的拷贝,最后进行汇总,就有些强人所难了。
你还可以在结构体中使用线程局部变量:
```rust
use std::cell::RefCell;
@ -354,6 +382,7 @@ fn main() {
```
或者通过引用的方式使用它:
```rust
use std::cell::RefCell;
use std::thread::LocalKey;
@ -374,7 +403,9 @@ impl Bar {
```
#### 三方库 thread-local
除了标准库外,一位大神还开发了 [thread-local](https://github.com/Amanieu/thread_local-rs) 库,它允许每个线程持有值的独立拷贝:
```rust
use thread_local::ThreadLocal;
use std::sync::Arc;
@ -403,9 +434,10 @@ assert_eq!(total, 5);
该库不仅仅使用了值的拷贝,而且还能自动把多个拷贝汇总到一个迭代器中,最后进行求和,非常好用。
## 用条件控制线程的挂起和执行
条件变量(Condition Variables)经常和 `Mutex` 一起使用,可以让线程挂起,直到某个条件发生后再继续执行:
```rust
use std::thread;
use std::sync::{Arc, Mutex, Condvar};
@ -438,7 +470,9 @@ fn main() {
2. 子线程获取到锁,并将其修改为 `true`,然后调用条件变量的 `notify_one` 方法来通知主线程继续执行
## 只被调用一次的函数
有时,我们会需要某个函数在多线程环境下只被调用一次,例如初始化全局变量,无论是哪个线程先调用函数来初始化,都会保证全局变量只会被初始化一次,随后的其它线程调用就会忽略该函数:
```rust
use std::thread;
use std::sync::{Once, ONCE_INIT};
@ -480,8 +514,8 @@ fn main() {
当这个函数返回时,保证一些初始化已经运行并完成,它还保证由执行的闭包所执行的任何内存写入都能被其他线程在这时可靠地观察到。
## 总结
[Rust 的线程模型](./intro.md)是 `1:1` 模型,因为 Rust 要保持尽量小的运行时。
我们可以使用 `thread::spawn` 来创建线程,创建出的多个线程之间并不存在执行顺序关系,因此代码逻辑千万不要依赖于线程间的执行顺序。

@ -1,9 +1,11 @@
# 错误处理
在之前的[返回值和错误处理](https://course.rs/basic/result-error/intro.html)章节中,我们学习了几个重要的概念,例如 `Result` 用于返回结果处理,`?` 用于错误的传播,若大家对此还较为模糊,强烈建议回头温习下。
在本章节中一起来看看如何对 `Result` ( `Option` ) 做进一步的处理,以及如何定义自己的错误类型。
## 组合器
在设计模式中,有一个组合器模式,相信有 Java 背景的同学对此并不陌生。
> 将对象组合成树形结构以表示“部分整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。GoF <<设计模式>>
@ -13,6 +15,7 @@
下面我们来看看一些常见的组合器。
#### or() 和 and()
跟布尔关系的与/或很像,这两个方法会对两个表达式做逻辑组合,最终返回 `Option` / `Result`
- `or()`,表达式按照顺序求值,若任何一个表达式的结果是 `Some``Ok`,则该值会立刻返回
@ -56,6 +59,7 @@ fn main() {
除了 `or``and` 之外Rust 还为我们提供了 `xor` ,但是它只能应用在 `Option` 上,其实想想也是这个理,如果能应用在 `Result` 上,那你又该如何对一个值和错误进行异或操作?
#### or_else() 和 and_then()
它们跟 `or()``and()` 类似,唯一的区别在于,它们的第二个表达式是一个闭包。
```rust
@ -121,7 +125,9 @@ fn main() {
```
#### filter
`filter` 用于对 `Option` 进行过滤:
```rust
fn main() {
let s1 = Some(3);
@ -137,7 +143,9 @@ fn main() {
```
#### map() 和 map_err()
`map` 可以将 `Some``Ok` 中的值映射为另一个:
```rust
fn main() {
let s1 = Some("abcde");
@ -163,6 +171,7 @@ fn main() {
```
但是如果你想要将 `Err` 中的值进行改变, `map` 就无能为力了,此时我们需要用 `map_err`
```rust
fn main() {
let o1: Result<&str, &str> = Ok("abcde");
@ -181,7 +190,9 @@ fn main() {
通过对 `o1` 的操作可以看出,与 `map` 面对 `Err` 时的短小类似, `map_err` 面对 `Ok` 时也是相当无力的。
#### map_or() 和 map_or_else()
`map_or``map` 的基础上提供了一个默认值:
```rust
fn main() {
const V_DEFAULT: u32 = 1;
@ -198,13 +209,14 @@ fn main() {
如上所示,当处理 `None` 的时候,`V_DEFAULT` 作为默认值被直接返回。
`map_or_else``map_or` 类似,但是它是通过一个闭包来提供默认值:
```rust
fn main() {
let s = Some(10);
let n: Option<i8> = None;
let fn_closure = |v: i8| v + 2;
let fn_default = || 1;
let fn_default = || 1;
assert_eq!(s.map_or_else(fn_default, fn_closure), 12);
assert_eq!(n.map_or_else(fn_default, fn_closure), 1);
@ -219,7 +231,9 @@ fn main() {
```
#### ok_or() and ok_or_else()
这两兄弟可以将 `Option` 类型转换为 `Result` 类型。其中 `ok_or` 接收一个默认的 `Err` 参数:
```rust
fn main() {
const ERR_DEFAULT: &str = "error message";
@ -236,6 +250,7 @@ fn main() {
```
`ok_or_else` 接收一个闭包作为 `Err` 参数:
```rust
fn main() {
let s = Some("abcde");
@ -253,9 +268,11 @@ fn main() {
以上列出的只是常用的一部分,强烈建议大家看看标准库中有哪些可用的 API在实际项目中这些 API 将会非常有用: [Option](https://doc.rust-lang.org/stable/std/option/enum.Option.html) 和 [Result](https://doc.rust-lang.org/stable/std/result/enum.Result.html)。
## 自定义错误类型
虽然标准库定义了大量的错误类型,但是一个严谨的项目,光使用这些错误类型往往是不够的,例如我们可能会为暴露给用户的错误定义相应的类型。
为了帮助我们更好的定义错误Rust 在标准库中提供了一些可复用的特征,例如 `std::error::Error` 特征:
```rust
use std::fmt::{Debug, Display};
@ -269,6 +286,7 @@ pub trait Error: Debug + Display {
> 实际上,自定义错误类型只需要实现 `Debug``Display` 特征即可,`source` 方法是可选的,而 `Debug` 特征往往也无需手动实现,可以直接通过 `derive` 来派生
#### 最简单的错误
```rust
use std::fmt;
@ -291,7 +309,7 @@ fn produce_error() -> Result<(), AppError> {
fn main(){
match produce_error() {
Err(e) => eprintln!("{}", e),
Err(e) => eprintln!("{}", e),
_ => println!("No error"),
}
@ -307,7 +325,9 @@ fn main(){
- 可以将自定义错误转换成 `Box<dyn std::error:Error>` 特征对象,在后面的**归一化不同错误类型**部分,我们会详细介绍
#### 更详尽的错误
上一个例子中定义的错误非常简单,我们无法从错误中得到更多的信息,现在再来定义一个具有错误码和信息的错误:
```rust
use std::fmt;
@ -363,9 +383,11 @@ fn main() {
在本例中,我们除了增加了错误码和消息外,还手动实现了 `Debug` 特征,原因在于,我们希望能自定义 `Debug` 的输出内容,而不是使用派生后系统提供的默认输出形式。
#### 错误转换 `From` 特征
标准库、三方库、本地库,各有各的精彩,各也有各的错误。那么问题就来了,我们该如何将其它的错误类型转换成自定义的错误类型?总不能神鬼牛魔,同台共舞吧。。
好在 Rust 为我们提供了 `std::convert::From` 特征:
```rust
pub trait From<T>: Sized {
fn from(_: T) -> Self;
@ -373,10 +395,11 @@ pub trait From<T>: Sized {
```
> 事实上,该特征在之前的 [`?` 操作符](https://course.rs/basic/result-error/result.html#传播界的大明星-)章节中就有所介绍。
>
>
> 大家都使用过 `String::from` 函数吧?它可以通过 `&str` 来创建一个 `String`,其实该函数就是 `From` 特征提供的
下面一起来看看如何为自定义类型实现 `From` 特征:
```rust
use std::fs::File;
use std::io;
@ -399,7 +422,7 @@ impl From<io::Error> for AppError {
}
fn main() -> Result<(), AppError> {
let _file = File::open("nonexistent_file.txt")?;
let _file = File::open("nonexistent_file.txt")?;
Ok(())
}
@ -411,6 +434,7 @@ Error: AppError { kind: "io", message: "No such file or directory (os error 2)"
上面的代码中除了实现 `From` 外,还有一点特别重要,那就是 `?` 可以将错误进行隐式的强制转换:`File::open` 返回的是 `std::io::Error` 我们并没有进行任何显式的转换,它就能自动变成 `AppError` ,这就是 `?` 的强大之处!
上面的例子只有一个标准库错误,再来看看多个不同的错误转换成 `AppError` 的实现:
```rust
use std::fs::File;
use std::io::{self, Read};
@ -444,10 +468,10 @@ fn main() -> Result<(), AppError> {
let mut file = File::open("hello_world.txt")?;
let mut content = String::new();
file.read_to_string(&mut content)?;
file.read_to_string(&mut content)?;
let _number: usize;
_number = content.parse()?;
_number = content.parse()?;
Ok(())
}
@ -466,6 +490,7 @@ Error: AppError { kind: "parse", message: "invalid digit found in string" }
```
## 归一化不同的错误类型
至此,关于 Rust 的错误处理大家已经了若指掌了,下面再来看看一些实战中的问题。
在实际项目中,我们往往会为不同的错误定义不同的类型,这样做非常好,但是如果你要在一个函数中返回不同的错误呢?例如:
@ -493,10 +518,11 @@ fn render() -> Result<String, std::io::Error> {
- 使用特征对象 `Box<dyn Error>`
- 自定义错误类型
- 使用 `thiserror`
下面依次来看看相关的解决方式。
#### Box<dyn Error>
大家还记得我们之前提到的 `std::error::Error` 特征吧,当时有说:自定义类型实现 `Debug + Display` 特征的主要原因就是为了能转换成 `Error` 的特征对象,而特征对象恰恰是在同一个地方使用不同类型的关键:
```rust
@ -518,7 +544,9 @@ fn render() -> Result<String, Box<dyn Error>> {
这个方法很简单,在绝大多数场景中,性能也非常够用,但是有一个问题:`Result` 实际上不会限制错误的类型,也就是一个类型就算不实现 `Error` 特征,它依然可以在 `Result<T, E>` 中作为 `E` 来使用,此时这种特征对象的解决方案就无能为力了。
#### 自定义错误类型
与特征对象相比,自定义错误类型麻烦归麻烦,但是它非常灵活,因此也不具有上面的类似限制:
```rust
use std::fs::read_to_string;
@ -571,10 +599,13 @@ impl std::fmt::Display for MyError {
上面的第二种方式灵活归灵活,啰嗦也是真啰嗦,好在 Rust 的社区为我们提供了 `thiserror` 解决方案,下面一起来看看该如何简化 Rust 中的错误处理。
## 简化错误处理
对于开发者而言,错误处理是代码中打交道最多的部分之一,因此选择一把趁手的武器也很重要,它可以帮助我们节省大量的时间和精力,好钢应该用在代码逻辑而不是冗长的错误处理上。
#### thiserror
[`thiserror`](https://github.com/dtolnay/thiserror)可以帮助我们简化上面的第二种解决方案:
```rust
use std::fs::read_to_string;
@ -602,6 +633,7 @@ enum MyError {
如上所示,只要简单写写注释,就可以实现错误处理了,惊不惊喜?
#### error-chain
[`error-chain`](https://github.com/rust-lang-deprecated/error-chain) 也是简单好用的库,可惜不再维护了,但是我觉得它依然可以在合适的地方大放光彩,值得大家去了解下。
```rust
@ -629,8 +661,8 @@ fn render() -> Result<String> {
喏,简单吧?使用 `error-chain` 的宏你可以获得:`Error` 结构体,错误类型 `ErrorKind` 枚举 以及一个自定义的 `Result` 类型。
#### anyhow
[`anyhow`](https://github.com/dtolnay/anyhow) 和 `thiserror` 是同一个作者开发的,这里是作者关于 `anyhow``thiserror` 的原话:
> 如果你想要设计自己的错误类型,同时给调用者提供具体的信息时,就使用 `thiserror`,例如当你在开发一个三方库代码时。如果你只想要简单,就使用 `anyhow`,例如在自己的应用服务中。
@ -638,4 +670,5 @@ fn render() -> Result<String> {
本章的篇幅已经过长,因此就不具体介绍 `anyhow` 该如何使用,官方提供的例子已经足够详尽,这里就留给大家自己探索了 :)
## 总结
Rust 一个为人津津乐道的点就是强大、易用的错误处理,对于新手来说,这个机制可能会有些复杂,但是一旦体会到了其中的好处,你将跟我一样沉醉其中不能自拔。

@ -3,6 +3,7 @@
闭包这个词语由来已久,自上世纪 60 年代就由 `Scheme` 语言引进之后,被广泛用于函数式编程语言中,进入 21 世纪后,各种现代化的编程语言也都不约而同地把闭包作为核心特性纳入到语言设计中来。那么到底何为闭包?
闭包是**一种匿名函数,它可以赋值给变量也可以作为参数传递给其它函数,不同于函数的是,它允许捕获调用者作用域中的值**,例如:
```rust
fn main() {
let x = 1;
@ -19,7 +20,9 @@ fn main() {
## 使用闭包来简化代码
### 传统函数实现
想象一下,我们要进行健身,用代码怎么实现(写代码什么鬼,健身难道不应该去健身房嘛?答曰:健身太累了,还是虚拟健身好,点到为止)?这里是我的想法:
```rust
use std::thread;
use std::time::Duration;
@ -36,7 +39,7 @@ fn workout(intensity: u32, random_number: u32) {
println!(
"今天活力满满,先做 {} 个俯卧撑!",
muuuuu(intensity)
);
);
println!(
"旁边有妹子在看俯卧撑太low再来 {} 组卧推!",
muuuuu(intensity)
@ -65,7 +68,9 @@ fn main() {
可以看到,在健身时我们根据想要的强度来调整具体的动作,然后调用 `muuuuu` 函数来开始健身。这个程序本身很简单,没啥好说的,但是假如未来不用 `muuuuu` 函数了,是不是得把所有 `muuuuu` 都替换成,比如说 `woooo` ?如果 `muuuuu` 出现了几十次,那意味着我们要修改几十处地方。
#### 函数变量实现
一个可行的办法是,把函数赋值给一个变量,然后通过变量调用:
```rust
use std::thread;
use std::time::Duration;
@ -109,7 +114,6 @@ fn main() {
}
```
经过上面修改后,所有的调用都通过 `action` 来完成,若未来声(动)音(作)变了,只要修改为 `let action = woooo` 即可。
但是问题又来了,若 `intensity` 也变了怎么办?例如变成 `action(intensity + 1)`,那你又得哐哐哐修改几十处调用。
@ -119,6 +123,7 @@ fn main() {
#### 闭包实现
上面提到 `intensity` 要是变化怎么办,简单,使用闭包来捕获它,这是我们的拿手好戏:
```rust
use std::thread;
use std::time::Duration;
@ -163,6 +168,7 @@ fn main() {
在上面代码中,无论你要修改什么,只要修改闭包 `action` 的实现即可,其它地方只负责调用,完美解决了我们的问题!
Rust 闭包在形式上借鉴了 `Smalltalk``Ruby` 语言,与函数最大的不同就是它的参数是通过 `|parm1|` 的形式进行声明,如果是多个参数就 `|param1, param2,...|` 下面给出闭包的形式定义:
```rust
|param1, param2,...| {
语句1;
@ -172,6 +178,7 @@ Rust 闭包在形式上借鉴了 `Smalltalk` 和 `Ruby` 语言,与函数最大
```
如果只有一个返回表达式的话,定义可以简化为:
```rust
|param1| 返回表达式
```
@ -182,11 +189,13 @@ Rust 闭包在形式上借鉴了 `Smalltalk` 和 `Ruby` 语言,与函数最大
- `let action = ||...` 只是把闭包赋值给变量 `action`,并不是把闭包执行后的结果赋值给 `action`,因此这里 `action` 就相当于闭包函数,可以跟函数一样进行调用:`action()`
## 闭包的类型推导
Rust 是静态语言,因此所有的变量都具有类型,但是得益于编译器的强大类型推导能力,在很多时候我们并不需要显式地去声明类型,但是显然函数并不在此列,必须手动为函数的所有参数和返回值指定类型,原因在于函数往往会作为 API 提供给你的用户,因此你的用户必须在使用时知道传入参数的类型和返回值类型。
与函数相反,闭包并不会作为 API 对外提供,因此它可以享受编译器的类型推导能力,无需标注参数和返回值的类型。
为了增加代码可读性,有时候我们会显式地给类型进行标注,出于同样的目的,也可以给闭包标注类型:
```rust
let sum = |x: i32, y: i32| -> i32 {
x + y
@ -194,6 +203,7 @@ let sum = |x: i32, y: i32| -> i32 {
```
与之相比,不标注类型的闭包声明会更简洁些:`let sum = |x, y| x + y`,需要注意的是,针对 `sum` 闭包,如果你只进行了声明,但是没有使用,编译器会提示你为 `x, y` 添加类型标注,因为它缺乏必要的上下文:
```rust
let sum = |x, y| x + y;
let v = sum(1, 2);
@ -202,6 +212,7 @@ let v = sum(1, 2);
这里我们使用了 `sum`,同时把 `1` 传给了 `x``2` 传给了 `y`,因此编译器才可以推导出 `x,y` 的类型为 `i32`
下面展示了同一个功能的函数和闭包实现形式:
```rust
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
@ -212,6 +223,7 @@ let add_one_v4 = |x| x + 1 ;
可以看出第一行的函数和后面的闭包其实在形式上是非常接近的,同时三种不同的闭包也展示了三种不同的使用方式:省略参数、返回值和花括号对。
虽然类型推导很好用,但是它不是泛型,**当编译器推导出一种类型后,它就会一直使用该类型**
```rust
let example_closure = |x| x;
@ -220,6 +232,7 @@ let n = example_closure(5);
```
首先,在 `s` 中,编译器为 `x` 推导出类型 `String`,但是紧接着 `n` 试图用 `5` 这个整型去调用闭包,跟编译器之前推导的 `String` 类型不符,因此报错:
```console
error[E0308]: mismatched types
--> src/main.rs:5:29
@ -232,12 +245,14 @@ error[E0308]: mismatched types
```
## 结构体中的闭包
假设我们要实现一个简易缓存,功能是获取一个值,然后将其缓存起来,那么可以这样设计:
- 一个闭包用于获取值
- 一个变量,用于存储该值
可以使用结构体来代表缓存对象,最终设计如下:
```rust
struct Cacher<T>
where
@ -261,6 +276,7 @@ where
> 需要注意的是,其实 Fn 特征不仅仅适用于闭包,还适用于函数,因此上面的 `query` 字段除了使用闭包作为值外,还能使用一个具名的函数来作为它的值
接着,为缓存实现方法:
```rust
impl<T> Cacher<T>
where
@ -289,9 +305,10 @@ where
上面的缓存有一个很大的问题:只支持 `u32` 类型的值,若我们想要缓存 `String` 类型,显然就行不通了,因此需要将 `u32` 替换成泛型 `E`,该练习就留给读者自己完成,具体代码可以参考[这里](http://exercise.rs/functional-programming/closure.html)
## 捕获作用域中的值
在之前代码中,我们一直在用闭包的匿名函数特性(赋值给变量),然而闭包还拥有一项函数所不具备的特性:捕获作用域中的值。
```rust
fn main() {
let x = 4;
@ -307,6 +324,7 @@ fn main() {
上面代码中,`x` 并不是闭包 `equal_to_x` 的参数,但是它依然可以去使用 `x`,因为 `equal_to_x``x` 的作用域范围内。
对于函数来说,就算你把函数定义在 `main` 函数体中,它也不能访问 `x`
```rust
fn main() {
let x = 4;
@ -322,6 +340,7 @@ fn main() {
```
报错如下:
```console
error[E0434]: can't capture dynamic environment in a fn item // 在函数中无法捕获动态的环境
--> src/main.rs:5:14
@ -335,9 +354,11 @@ error[E0434]: can't capture dynamic environment in a fn item // 在函数中无
如上所示,编译器准确地告诉了我们错误,甚至同时给出了提示:使用闭包来替代函数,这种聪明令我有些无所适从,总感觉会显得我很笨。
### 闭包对内存的影响
当闭包从环境中捕获一个值时,会分配内存去存储这些值。对于有些场景来说,这种额外的内存分配会成为一种负担。与之相比,函数就不会去捕获这些环境值,因此定义和使用函数不会拥有这种内存负担。
### 三种 Fn 特征
闭包捕获变量有三种途径,恰好对应函数参数的三种传入方式:转移所有权、可变借用、不可变借用,因此相应的 `Fn` 特征也有三种:
1. `FnOnce`,该类型的闭包会拿走被捕获变量的所有权。`Once` 顾名思义,说明该闭包只能运行一次:
@ -394,28 +415,30 @@ fn main() {
上面代码中,`func` 的类型 `F` 实现了 `Copy` 特征,调用时使用的将是它的拷贝,所以并没有发生所有权的转移。
```console
true
true
false
```
如果你想强制闭包取得捕获变量的所有权,可以在参数列表前添加 `move` 关键字,这种用法通常用于闭包的生命周期大于捕获变量的生命周期时,例如将闭包返回或移入其他线程。
2. `FnMut`,它以可变借用的方式捕获了环境中的值,因此可以修改该值:
```rust
fn main() {
let mut s = String::new();
let update_string = |str| s.push_str(str);
update_string("hello");
println!("{:?}",s);
}
```
在闭包中,我们调用 `s.push_str` 去改变外部 `s` 的字符串值,因此这里捕获了它的可变借用,运行下试试:
```console
error[E0596]: cannot borrow `update_string` as mutable, as it is not declared as mutable
--> src/main.rs:5:5
--> src/main.rs:5:5
|
4 | let update_string = |str| s.push_str(str);
| ------------- - calling `update_string` requires mutable binding due to mutable borrow of `s`
@ -426,13 +449,14 @@ error[E0596]: cannot borrow `update_string` as mutable, as it is not declared as
```
虽然报错了,但是编译器给出了非常清晰的提示,想要在闭包内部捕获可变借用,需要把该闭包声明为可变类型,也就是 `update_string` 要修改为 `mut update_string`
```rust
fn main() {
let mut s = String::new();
let mut update_string = |str| s.push_str(str);
update_string("hello");
println!("{:?}",s);
}
```
@ -440,14 +464,15 @@ fn main() {
这种写法有点反直觉,相比起来前面的 `move` 更符合使用和阅读习惯。但是如果你忽略 `update_string` 的类型,仅仅把它当成一个普通变量,那么这种声明就比较合理了。
再来看一个复杂点的:
```rust
fn main() {
let mut s = String::new();
let update_string = |str| s.push_str(str);
exec(update_string);
println!("{:?}",s);
}
@ -459,15 +484,16 @@ fn exec<'a, F: FnMut(&'a str)>(mut f: F) {
这段代码非常清晰的说明了 `update_string` 实现了 `FnMut` 特征
3. `Fn` 特征,它以不可变借用的方式捕获环境中的值
让我们把上面的代码中 `exec``F` 泛型参数类型修改为 `Fn(&'a str)`
让我们把上面的代码中 `exec``F` 泛型参数类型修改为 `Fn(&'a str)`
```rust
fn main() {
let mut s = String::new();
let update_string = |str| s.push_str(str);
exec(update_string);
println!("{:?}",s);
}
@ -487,20 +513,21 @@ error[E0525]: expected a closure that implements the `Fn` trait, but this closur
| | |
| | closure is `FnMut` because it mutates the variable `s` here
| this closure implements `FnMut`, not `Fn` //闭包实现的是FnMut而不是Fn
5 |
5 |
6 | exec(update_string);
| ---- the requirement to implement `Fn` derives from here
```
从报错中很清晰的看出,我们的闭包实现的是 `FnMut` 特征,需要的是可变借用,但是在 `exec` 中却给它标注了 `Fn` 特征,因此产生了不匹配,再来看看正确的不可变借用方式:
从报错中很清晰的看出,我们的闭包实现的是 `FnMut` 特征,需要的是可变借用,但是在 `exec` 中却给它标注了 `Fn` 特征,因此产生了不匹配,再来看看正确的不可变借用方式:
```rust
fn main() {
let s = "hello, ".to_string();
let update_string = |str| println!("{},{}",s,str);
exec(update_string);
println!("{:?}",s);
}
@ -511,17 +538,18 @@ fn exec<'a, F: Fn(String) -> ()>(f: F) {
在这里,因为无需改变 `s`,因此闭包中只对 `s` 进行了不可变借用,那么在 `exec` 中,将其标记为 `Fn` 特征就完全正确。
#### move 和 Fn
在上面,我们讲到了 `move` 关键字对于 `FnOnce` 特征的重要性,但是实际上使用了 `move` 的闭包依然可能实现了 `Fn``FnMut` 特征。
因为,**一个闭包实现了哪种 Fn 特征取决于该闭包如何使用被捕获的变量,而不是取决于闭包如何捕获它们**。`move` 本身强调的就是后者,闭包如何捕获变量:
```rust
fn main() {
let s = String::new();
let update_string = move || println!("{}",s);
exec(update_string);
}
@ -533,12 +561,13 @@ fn exec<F: FnOnce()>(f: F) {
我们在上面的闭包中使用了 `move` 关键字,所以我们的闭包捕获了它,但是由于闭包对 `s` 的使用仅仅是不可变借用,因此该闭包实际上**还**实现了 `Fn` 特征。
细心的读者肯定发现我在上段中使用了一个 `还` 字,这是什么意思呢?因为该闭包不仅仅实现了 `FnOnce` 特征,还实现了 `Fn` 特征,将代码修改成下面这样,依然可以编译:
```rust
fn main() {
let s = String::new();
let update_string = move || println!("{}",s);
exec(update_string);
}
@ -548,6 +577,7 @@ fn exec<F: Fn()>(f: F) {
```
#### 三种 Fn 的关系
实际上,一个闭包并不仅仅实现某一种 `Fn` 特征,规则如下:
- 所有的闭包都自动实现了 `FnOnce` 特征,因此任何一个闭包都至少可以被调用一次
@ -555,12 +585,13 @@ fn exec<F: Fn()>(f: F) {
- 不需要对捕获变量进行改变的闭包自动实现了 `Fn` 特征
用一段代码来简单诠释上述规则:
```rust
fn main() {
let s = String::new();
let update_string = || println!("{}",s);
exec(update_string);
exec1(update_string);
exec2(update_string);
@ -608,6 +639,7 @@ fn exec<'a, F: FnMut(&'a str) -> String>(mut f: F) {
此例中,闭包从捕获环境中移出了变量 `s` 的所有权,因此这个闭包仅自动实现了 `FnOnce`,未实现 `FnMut``Fn`。再次印证之前讲的**一个闭包实现了哪种 Fn 特征取决于该闭包如何使用被捕获的变量,而不是取决于闭包如何捕获它们**,跟是否使用 `move` 没有必然联系。
如果还是有疑惑?没关系,我们来看看这三个特征的简化版源码:
```rust
pub trait Fn<Args> : FnMut<Args> {
extern "rust-call" fn call(&self, args: Args) -> Self::Output;
@ -630,9 +662,11 @@ pub trait FnOnce<Args> {
在实际项目中,**建议先使用 `Fn` 特征**,然后编译器会告诉你正误以及该如何选择。
## 闭包作为函数返回值
看到这里,相信大家对于如何使用闭包作为函数参数,已经很熟悉了,但是如果要使用闭包作为函数返回值,该如何做?
先来看一段代码:
```rust
fn factory() -> Fn(i32) -> i32 {
let num = 5;
@ -647,14 +681,16 @@ assert_eq!(6, answer);
```
上面这段代码看起来还是蛮正常的,用 `Fn(i32) -> i32` 特征来代表 `|x| x + num`,非常合理嘛,肯定可以编译通过, 可惜理想总是难以照进现实,编译器给我们报了一大堆错误,先挑几个重点来看看:
```console
fn factory<T>() -> Fn(i32) -> i32 {
| ^^^^^^^^^^^^^^ doesn't have a size known at compile-time // 该类型在编译器没有固定的大小
```
Rust 要求函数的参数和返回类型,必须有固定的内存大小,例如 `i32` 就是4个字节引用类型是8个字节总之绝大部分类型都有固定的大小但是不包括特征因为特征类似接口对于编译器来说无法知道它后面藏的真实类型是什么因为也无法得知具体的大小。
Rust 要求函数的参数和返回类型,必须有固定的内存大小,例如 `i32` 就是 4 个字节,引用类型是 8 个字节,总之,绝大部分类型都有固定的大小,但是不包括特征,因为特征类似接口,对于编译器来说,无法知道它后面藏的真实类型是什么,因为也无法得知具体的大小。
同样,我们也无法知道闭包的具体类型,该怎么办呢?再看看报错提示:
```console
help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/main.rs:11:5: 11:21]`, which implements `Fn(i32) -> i32`
|
@ -664,6 +700,7 @@ help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of t
嗯,编译器提示我们加一个 `impl` 关键字,哦,这样一说,读者可能就想起来了,`impl Trait` 可以用来返回一个实现了指定特征的类型,那么这里 `impl Fn(i32) -> i32` 的返回值形式,说明我们要返回一个闭包类型,它实现了 `Fn(i32) -> i32` 特征。
完美解决,但是,在[特征](../../basic/trait/trait.md)那一章,我们提到过,`impl Trait` 的返回方式有一个非常大的局限,就是你只能返回同样的类型,例如:
```rust
fn factory(x:i32) -> impl Fn(i32) -> i32 {
@ -676,7 +713,9 @@ fn factory(x:i32) -> impl Fn(i32) -> i32 {
}
}
```
运行后,编译器报错:
```console
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:15:9
@ -695,11 +734,13 @@ error[E0308]: `if` and `else` have incompatible types
嗯,提示很清晰:`if` 和 `else` 分支中返回了不同的闭包类型,这就很奇怪了,明明这两个闭包长的一样的,好在细心的读者应该回想起来,本章节前面咱们有提到:就算签名一样的闭包,类型也是不同的,因此在这种情况下,就无法再使用 `impl Trait` 的方式去返回闭包。
怎么办?再看看编译器提示,里面有这样一行小字:
```console
= help: consider boxing your closure and/or using it as a trait object
```
哦,相信你已经恍然大悟,可以用特征对象!只需要用 `Box` 的方式即可实现:
```rust
fn factory(x:i32) -> Box<dyn Fn(i32) -> i32> {
let num = 5;
@ -715,4 +756,5 @@ fn factory(x:i32) -> Box<dyn Fn(i32) -> i32> {
至此,闭包作为函数返回值就已完美解决,若以后你再遇到报错时,一定要仔细阅读编译器的提示,很多时候,转角都能遇到爱。
## 闭包的生命周期
这块儿内容在进阶生命周期章节中有讲,这里就不再赘述,读者可移步[此处](https://course.rs/advance/lifetime/advance.html#闭包函数的消除规则)进行回顾。

@ -10,13 +10,9 @@
关于函数式编程到底是什么的争论由来已久,本章节并不会踏足这个泥潭,因此我们在这里主要关注的是函数式特性:
- 闭包closure
- 迭代器iterator
- 闭包 Closure
- 迭代器 Iterator
- 模式匹配
- 枚举
其中后两个在前面章节我们已经深入学习过,因此本章的重点就是闭包和迭代器,**这些函数式特性可以让代码的可读性和易写性大幅提升**。对于 Rust 语言来说,掌握这两者就相当于你同时拥有了倚天剑屠龙刀,威力无穷。

@ -1,20 +1,24 @@
# 迭代器 Iterator
如果你询问一个 Rust 资深开发:写 Rust 项目最需要掌握什么?相信迭代器往往就是答案之一。无论你是编程新手亦或是高手,实际上大概率都用过迭代器,虽然自己可能并没有意识到这一点:)
迭代器允许我们迭代一个连续的集合,例如数组、动态数组 `Vec`、`HashMap` 等,在此过程中,只需关心集合中的元素如何处理,而无需关心如何开始、如何结束、按照什么样的索引去访问等问题。
## For 循环与迭代器
从用途来看,迭代器跟 `for` 循环颇为相似,都是去遍历一个集合,但是实际上它们存在不小的差别,其中最主要的差别就是:**是否通过索引来访问集合**。
例如以下的 JS 代码就是一个循环:
```javascript
let arr = [1, 2, 3];
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
console.log(arr[i]);
}
```
在上面代码中,我们设置索引的开始点和结束点,然后再通过索引去访问元素 `arr[i]`,这就是典型的循环,来对比下 Rust 中的 `for`
```rust
let arr = [1, 2, 3];
for v in arr {
@ -27,14 +31,17 @@ for v in arr {
那又有同学要发问了,在 Rust 中数组是迭代器吗?因为在之前的代码中直接对数组 `arr` 进行了迭代,答案是 `No`。那既然数组不是迭代器,为啥咱可以对它的元素进行迭代呢?
简而言之就是数组实现了 `IntoIterator` 特征Rust 通过 `for` 语法糖,自动把实现了该特征的数组类型转换为迭代器(你也可以为自己的集合类型实现此特征),最终让我们可以直接对一个数组进行迭代,类似的还有:
```rust
for i in 1..10 {
println!("{}", i);
}
```
直接对数值序列进行迭代,也是很常见的使用方式。
`IntoIterator` 特征拥有一个 `into_iter` 方法,因此我们还可以显式的把数组转换成迭代器:
```rust
let arr = [1, 2, 3];
for v in arr.into_iter() {
@ -45,7 +52,9 @@ for v in arr.into_iter() {
迭代器是函数语言的核心特性,它赋予了 Rust 远超于循环的强大表达能力,我们将在本章中一一为大家进行展现。
## 惰性初始化
在 Rust 中,迭代器是惰性的,意味着如果你不使用它,那么它将不会发生任何事:
```rust
let v1 = vec![1, 2, 3];
@ -61,9 +70,11 @@ for val in v1_iter {
这种惰性初始化的方式确保了创建迭代器不会有任何额外的性能损耗,其中的元素也不会被消耗,只有使用到该迭代器的时候,一切才开始。
## next 方法
对于 `for` 如何遍历迭代器,还有一个问题,它如何取出迭代器中的元素?
先来看一个特征:
```rust
pub trait Iterator {
type Item;
@ -79,6 +90,7 @@ pub trait Iterator {
因此,之前问题的答案已经很明显:`for` 循环通过不停调用迭代器上的 `next` 方法,来获取迭代器中的元素。
既然 `for` 可以调用 `next` 方法,是不是意味着我们也可以?来试试:
```rust
fn main() {
let arr = [1, 2, 3];
@ -99,8 +111,10 @@ fn main() {
总之,`next` 方法对**迭代器的遍历是消耗性的**,每次消耗它一个元素,最终迭代器中将没有任何元素,只能返回 `None`
#### 例子模拟实现for循环
#### 例子:模拟实现 for 循环
因为 `for` 循环是迭代器的语法糖,因此我们完全可以通过迭代器来模拟实现它:
```rust
let values = vec![1, 2, 3];
@ -122,7 +136,9 @@ let values = vec![1, 2, 3];
同时我们使用了 `loop` 循环配合 `next` 方法来遍历迭代器中的元素,当迭代器返回 `None` 时,跳出循环。
## IntoIterator 特征
其实有一个细节,由于 `Vec` 动态数组实现了 `IntoIterator` 特征,因此可以通过 `into_iter` 将其转换为迭代器,那如果本身就是一个迭代器,该怎么办?实际上,迭代器自身也实现了 `IntoIterator`,标准库早就帮我们考虑好了:
```rust
impl<I: Iterator> IntoIterator for I {
type Item = I::Item;
@ -136,6 +152,7 @@ impl<I: Iterator> IntoIterator for I {
```
最终你完全可以写出这样的奇怪代码:
```rust
fn main() {
let values = vec![1, 2, 3];
@ -147,6 +164,7 @@ fn main() {
```
#### into_iter, iter, iter_mut
在之前的代码中,我们统一使用了 `into_iter` 的方式将数组转化为迭代器,除此之外,还有 `iter``iter_mut`,聪明的读者应该大概能猜到这三者的区别:
- `into_iter` 会夺走所有权
@ -156,6 +174,7 @@ fn main() {
其实如果以后见多识广了,你会发现这种问题一眼就能看穿,`into_` 之类的,都是拿走所有权,`_mut` 之类的都是可变借用,剩下的就是不可变借用。
使用一段代码来解释下:
```rust
fn main() {
let values = vec![1, 2, 3];
@ -186,23 +205,28 @@ fn main() {
println!("{:?}", values);
}
```
具体解释在代码注释中,就不再赘述,不过有两点需要注意的是:
- `.iter()` 方法实现的迭代器,调用 `next` 方法返回的类型是 `Some(&T)`
- `.iter_mut()` 方法实现的迭代器,调用 `next` 方法返回的类型是 `Some(&mut T)`,因此在 `if let Some(v) = values_iter_mut.next()` 中,`v` 的类型是 `&mut i32`,最终我们可以通过 `*v = 0` 的方式修改其值
#### Iterator 和 IntoIterator 的区别
这两个其实还蛮容易搞混的,但我们只需要记住,`Iterator` 就是迭代器特征,只有实现了它才能称为迭代器,才能调用 `next`
`IntoIterator` 强调的是某一个类型如果实现了该特征,它可以通过 `into_iter``iter` 等方法变成一个迭代器。
## 消费者与适配器
消费者是迭代器上的方法,它会消费掉迭代器中的元素,然后返回其类型的值,这些消费者都有一个共同的特点:在它们的定义中,都依赖 `next` 方法来消费元素,因此这也是为什么迭代器要实现 `Iterator` 特征,而该特征必须要实现 `next` 方法的原因。
#### 消费者适配器
只要迭代器上的某个方法 `A` 在其内部调用了 `next` 方法,那么 `A` 就被称为**消费性适配器**:因为 `next` 方法会消耗掉迭代器上的元素,所以方法 `A` 的调用也会消耗掉迭代器上的元素。
其中一个例子是 `sum` 方法,它会拿走迭代器的所有权,然后通过不断调用 `next` 方法对里面的元素进行求和:
```rust
fn main() {
let v1 = vec![1, 2, 3];
@ -212,7 +236,7 @@ fn main() {
let total: i32 = v1_iter.sum();
assert_eq!(total, 6);
// v1_iter 是借用了 v1因此 v1 可以照常使用
println!("{:?}",v1);
@ -222,6 +246,7 @@ fn main() {
```
如代码注释中所说明的:在使用 `sum` 方法后,我们将无法再使用 `v1_iter`,因为 `sum` 拿走了该迭代器的所有权:
```rust
fn sum<S>(self) -> S
where
@ -236,9 +261,11 @@ fn sum<S>(self) -> S
`sum` 源码中也可以清晰看出,`self` 类型的方法参数拿走了所有权。
#### 迭代器适配器
既然消费者适配器是消费掉迭代器,然后返回一个值。那么迭代器适配器,顾名思义,会返回一个新的迭代器,这是实现链式方法调用的关键:`v.iter().map().filter()...`。
与消费者适配器不同,迭代器适配器是惰性的,意味着你**需要一个消费者适配器来收尾,最终将迭代器转换成一个具体的值**
```rust
let v1: Vec<i32> = vec![1, 2, 3];
@ -246,6 +273,7 @@ v1.iter().map(|x| x + 1);
```
运行后输出:
```console
warning: unused `Map` that must be used
--> src/main.rs:4:5
@ -258,6 +286,7 @@ warning: unused `Map` that must be used
```
如上述中文注释所说,这里的 `map` 方法是一个迭代者适配器,它是惰性的,不产生任何行为,因此我们还需要一个消费者适配器进行收尾:
```rust
let v1: Vec<i32> = vec![1, 2, 3];
@ -267,6 +296,7 @@ assert_eq!(v2, vec![2, 3, 4]);
```
#### collect
上面代码中,使用了 `collect` 方法,该方法就是一个消费者适配器,使用它可以将一个迭代器中的元素收集到指定类型中,这里我们为 `v2` 标注了 `Vec<_>` 类型,就是为了告诉 `collect`:请把迭代器中的元素消费掉,然后把值收集成 `Vec<_>` 类型,至于为何使用 `_`,因为编译器会帮我们自动推导。
为何 `collect` 在消费时要指定类型?是因为该方法其实很强大,可以收集成多种不同的集合类型,`Vec<T>` 仅仅是其中之一,因此我们必须显式的告诉编译器我们想要收集成的集合类型。
@ -274,6 +304,7 @@ assert_eq!(v2, vec![2, 3, 4]);
还有一点值得注意,`map` 会对迭代器中的每一个值进行一系列操作,然后把该值转换成另外一个新值,该操作是通过闭包 `|x| x + 1` 来完成:最终迭代器中的每个值都增加了 `1`,从 `[1, 2, 3]` 变为 `[2, 3, 4]`
再来看看如何使用 `collect` 收集成 `HashMap` 集合:
```rust
use std::collections::HashMap;
fn main() {
@ -290,7 +321,9 @@ fn main() {
然后再通过 `collect` 将新迭代器中`(K, V)` 形式的值收集成 `HashMap<K, V>`,同样的,这里必须显式声明类型,然后 `HashMap` 内部的 `KV` 类型可以交给编译器去推导,最终编译器会推导出 `HashMap<&str, i32>`,完全正确!
#### 闭包作为适配器参数
之前的 `map` 方法中,我们使用闭包来作为迭代器适配器的参数,它最大的好处不仅在于可以就地实现迭代器中元素的处理,还在于可以捕获环境值:
```rust
struct Shoe {
size: u32,
@ -305,9 +338,11 @@ fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
`filter` 是迭代器适配器,用于对迭代器中的每个值进行过滤。 它使用闭包作为参数,该闭包的参数 `s` 是来自迭代器中的值,然后使用 `s` 跟外部环境中的 `shoe_size` 进行比较,若相等,则在迭代器中保留 `s` 值,若不相等,则从迭代器中剔除 `s` 值,最终通过 `collect` 收集为 `Vec<Shoe>` 类型。
## 实现 Iterator 特征
之前的内容我们一直基于数组来创建迭代器,实际上,不仅仅是数组,基于其它集合类型一样可以创建迭代器,例如 `HashMap`。 你也可以创建自己的迭代器 —— 只要为自定义类型实现 `Iterator` 特征即可。
首先,创建一个计数器:
```rust
struct Counter {
count: u32,
@ -321,6 +356,7 @@ impl Counter {
```
我们为计数器 `Counter` 实现了一个关联函数 `new`,用于创建新的计数器实例。下面们继续为计数器实现 `Iterator` 特征:
```rust
impl Iterator for Counter {
type Item = u32;
@ -338,9 +374,10 @@ impl Iterator for Counter {
首先,将该特征的关联类型设置为 `u32`,由于我们的计数器保存的 `count` 字段就是 `u32` 类型, 因此在 `next` 方法中,最后返回的是实际上是 `Option<u32>` 类型。
每次调用 `next` 方法都会让计数器的值加一然后返回最新的计数值一旦计数大于5就返回 `None`
每次调用 `next` 方法,都会让计数器的值加一,然后返回最新的计数值,一旦计数大于 5就返回 `None`
最后,使用我们新建的 `Counter` 进行迭代:
```rust
let mut counter = Counter::new();
@ -353,9 +390,11 @@ assert_eq!(counter.next(), None);
```
#### 实现 Iterator 特征的其它方法
可以看出,实现自己的迭代器非常简单,但是 `Iterator` 特征中,不仅仅是只有 `next` 一个方法,那为什么我们只需要实现它呢?因为其它方法都具有[默认实现](https://course.rs/basic/trait/trait.html#默认实现),所以无需像 `next` 这样手动去实现,而且这些默认实现的方法其实都是基于 `next` 方法实现的。
下面的代码演示了部分方法的使用:
```rust
let sum: u32 = Counter::new()
.zip(Counter::new().skip(1))
@ -374,7 +413,9 @@ assert_eq!(18, sum);
`sum` 是消费者适配器,对迭代器中的所有元素求和,最终返回一个 `u32``18`
##### enumerate
在之前的流程控制章节,针对 `for` 循环,我们提供了一种方法可以获取迭代时的索引:
```rust
let v = vec![1u64, 2, 3, 4, 5, 6];
for (i,v) in v.iter().enumerate() {
@ -386,6 +427,7 @@ for (i,v) in v.iter().enumerate() {
调用 `Iterator` 特征上的方法 `enumerate`,该方法产生一个新的迭代器,其中每个元素均是元组 `(索引,值)`
因为 `enumerate` 是迭代器适配器,因此我们可以对它返回的迭代器调用其它 `Iterator` 特征方法:
```rust
let v = vec![1u64, 2, 3, 4, 5, 6];
let val = v.iter()
@ -476,6 +518,7 @@ test bench::bench_iter ... bench: 983,858 ns/iter (+/- 44,673)
所以请放心大胆的使用迭代器,在获得更高的表达力的同时,也不会导致运行时的损失,何乐而不为呢!
## 学习其它方法
迭代器用的好不好,就在于你是否掌握了它的常用方法,且能活学活用,因此多多看看[标准库](https://doc.rust-lang.org/std/iter/trait.Iterator.html)是有好处的,只有知道有什么方法,在需要的时候你才能知道该用什么,就和算法学习一样。
同时,本书在后续章节还提供了对迭代器常用方法的[深入讲解](https://course.rs/std/iterator),方便大家学习和查阅。

@ -1,15 +1,19 @@
# 全局变量
在一些场景我们可能需要全局变量来简化状态共享的代码包括全局ID全局数据存储等等下面一起来看看有哪些创建全局变量的方法。
在一些场景,我们可能需要全局变量来简化状态共享的代码,包括全局 ID全局数据存储等等下面一起来看看有哪些创建全局变量的方法。
首先,有一点可以肯定,全局变量的生命周期肯定是`'static`,但是不代表它需要用`static`来声明,例如常量、字符串字面值等无需使用`static`进行声明,原因是它们已经被打包到二进制可执行文件中。
下面我们从编译期初始化及运行期初始化两个类别来介绍下全局变量有哪些类型及该如何使用。
## 编译期初始化
我们大多数使用的全局变量都只需要在编译期初始化即可,例如静态配置、计数器、状态值等等。
#### 静态常量
全局常量可以在程序任何一部分使用,当然,如果它是定义在某个模块中,你需要引入对应的模块才能使用。常量,顾名思义它是不可变的,很适合用作静态配置:
```rust
const MAX_ID: usize = usize::MAX / 2;
fn main() {
@ -18,15 +22,18 @@ fn main() {
```
**常量与普通变量的区别**
- 关键字是`const`而不是`let`
- 定义常量必须指明类型如i32不能省略
- 定义常量必须指明类型(如 i32不能省略
- 定义常量时变量的命名规则一般是全部大写
- 常量可以在任意作用域进行定义,其生命周期贯穿整个程序的生命周期。编译时编译器会尽可能将其内联到代码中,所以在不同地方对同一常量的引用并不能保证引用到相同的内存地址
- 常量的赋值只能是常量表达式/数学表达式,也就是说必须是在编译期就能计算出的值,如果需要在运行时才能得出结果的值比如函数,则不能赋值给常量表达式
- 对于变量出现重复的定义(绑定)会发生变量遮盖,后面定义的变量会遮住前面定义的变量,常量则不允许出现重复的定义
#### 静态变量
静态变量允许声明一个全局的变量,常用于全局数据统计,例如我们希望用一个变量来统计程序当前的总请求数:
```rust
static mut REQUEST_RECV: usize = 0;
fn main() {
@ -37,17 +44,19 @@ fn main() {
}
```
Rust要求必须使用`unsafe`语句块才能访问和修改`static`变量,因为这种使用方式往往并不安全,其实编译器是对的,当在多线程中同时去修改时,会不可避免的遇到脏数据。
Rust 要求必须使用`unsafe`语句块才能访问和修改`static`变量,因为这种使用方式往往并不安全,其实编译器是对的,当在多线程中同时去修改时,会不可避免的遇到脏数据。
只有在同一线程内或者不在乎数据的准确性时,才应该使用全局静态变量。
和常量相同,定义静态变量的时候必须赋值为在编译期就可以计算出的值(常量表达式/数学表达式),不能是运行时才能计算出的值(如函数)
**静态变量和常量的区别**
- 静态变量不会被内联,在整个程序中,静态变量只有一个实例,所有的引用都会指向同一个地址
- 存储在静态变量中的值必须要实现Sync trait
- 存储在静态变量中的值必须要实现 Sync trait
#### 原子类型
想要全局计数器、状态控制等功能,又想要线程安全的实现,原子类型是非常好的办法。
```rust
@ -57,16 +66,17 @@ fn main() {
for _ in 0..100 {
REQUEST_RECV.fetch_add(1, Ordering::Relaxed);
}
println!("当前用户请求数{:?}",REQUEST_RECV);
}
```
关于原子类型的讲解看[这篇文章](./concurrency-with-threads/sync2.md)
#### 示例:全局 ID 生成器
来看看如何使用上面的内容实现一个全局 ID 生成器:
#### 示例全局ID生成器
来看看如何使用上面的内容实现一个全局ID生成器:
```rust
use std::sync::atomic::{Ordering, AtomicUsize};
@ -99,9 +109,10 @@ impl Factory{
}
```
## 运行期初始化
以上的静态初始化有一个致命的问题:无法用函数进行静态初始化,例如你如果想声明一个全局的`Mutex`锁:
```rust
use std::sync::Mutex;
static names: Mutex<String> = Mutex::new(String::from("Sunface, Jack, Allen"));
@ -113,6 +124,7 @@ fn main() {
```
运行后报错如下:
```console
error[E0015]: calls in statics are limited to constant functions, tuple structs and tuple variants
--> src/main.rs:3:42
@ -123,6 +135,7 @@ error[E0015]: calls in statics are limited to constant functions, tuple structs
但你又必须在声明时就对`names`进行初始化,此时就陷入了两难的境地。好在天无绝人之路,我们可以使用`lazy_static`包来解决这个问题。
#### lazy_static
[`lazy_static`](https://github.com/rust-lang-nursery/lazy-static.rs)是社区提供的非常强大的宏,用于懒初始化静态变量,之前的静态变量都是在编译器初始化的,因此无法使用函数调用进行赋值,而`lazy_static`允许我们在运行期初始化静态变量!
```rust
@ -146,6 +159,7 @@ fn main() {
可能有读者会问,为何需要在运行期初始化一个静态变量,除了上面的全局锁,你会遇到最常见的场景就是:**一个全局的动态配置,它在程序开始后,才加载数据进行初始化,最终可以让各个线程直接访问使用**
再来看一个使用`lazy_static`实现全局缓存的例子:
```rust
use lazy_static::lazy_static;
use std::collections::HashMap;
@ -172,7 +186,9 @@ fn main() {
需要注意的是,`lazy_static`直到运行到`main`中的第一行代码时,才进行初始化,非常`lazy static`。
#### Box::leak
在`Box`智能指针章节中,我们提到了`Box::leak`可以用于全局变量,例如用作运行期初始化的全局动态配置,先来看看如果不使用`lazy_static`也不使用`Box::leak`,会发生什么:
```rust
#[derive(Debug)]
struct Config {
@ -189,11 +205,12 @@ fn main() {
});
println!("{:?}",config)
}
}
}
```
以上代码我们声明了一个全局动态配置`config`,并且其值初始化为`None`,然后在程序开始运行后,给它赋予相应的值,运行后报错:
```console
error[E0716]: temporary value dropped while borrowed
--> src/main.rs:10:28
@ -211,7 +228,7 @@ error[E0716]: temporary value dropped while borrowed
| creates a temporary which is freed while still in use
```
可以看到Rust的借用和生命周期规则限制了我们做到这一点因为试图将一个局部生命周期的变量赋值给全局生命周期的`config`,这明显是不安全的。
可以看到Rust 的借用和生命周期规则限制了我们做到这一点,因为试图将一个局部生命周期的变量赋值给全局生命周期的`config`,这明显是不安全的。
好在`Rust`为我们提供了`Box::leak`方法,它可以将一个变量从内存中泄漏(听上去怪怪的,竟然做主动内存泄漏),然后将其变为`'static`生命周期,最终该变量将和程序活得一样久,因此可以赋值给全局静态变量`config`。
@ -239,9 +256,10 @@ fn main() {
}
```
#### 从函数中返回全局变量
问题又来了,如果我们需要在运行期,从一个函数返回一个全局变量该如何做?例如:
```rust
#[derive(Debug)]
struct Config {
@ -263,11 +281,12 @@ fn main() {
config = init();
println!("{:?}",config)
}
}
}
```
报错这里就不展示了,跟之前大同小异,还是生命周期引起的,那么该如何解决呢?依然可以用`Box::leak`:
```rust
#[derive(Debug)]
struct Config {
@ -291,19 +310,19 @@ fn main() {
config = init();
println!("{:?}",config)
}
}
}
```
## 标准库中的OnceCell
## 标准库中的 OnceCell
@todo
## 总结
在Rust中有很多方式可以创建一个全局变量本章也只是介绍了其中一部分更多的还等待大家自己去挖掘学习(当然,未来可能本章节会不断完善,最后变成一个巨无霸- , -)。
在 Rust 中有很多方式可以创建一个全局变量,本章也只是介绍了其中一部分,更多的还等待大家自己去挖掘学习(当然,未来可能本章节会不断完善,最后变成一个巨无霸- , -)。
简单来说,全局变量可以分为两种:
- 编译期初始化的全局变量,`const`创建常量,`static`创建静态变量,`Atomic`创建原子类型
- 运行期初始化的全局变量,`lazy_static`用于懒初始化,`Box::leak`利用内存泄漏将一个变量的生命周期变为`'static`

@ -1,8 +1,10 @@
# 深入 Rust 类型
弱弱地、不负责任地说Rust 的学习难度之恶名,可能有一半来源于 Rust 的类型系统,而其中一半的一半则来自于本章节的内容。在本章,我们将重点学习如何创建自定义类型,以及了解何为动态大小的类型。
## newtype
何为 `newtype`?简单来说,就是使用[元组结构体](https://course.rs/basic/compound-type/struct.html#元组结构体tuple-struct)的方式将已有的类型包裹起来:`struct Meters(u32);`,那么此处 `Meters` 就是一个 `newtype`
何为 `newtype`?简单来说,就是使用[元组结构体](https://course.rs/basic/compound-type/struct.html#元组结构体tuple-struct)的方式将已有的类型包裹起来:`struct Meters(u32);`,那么此处 `Meters` 就是一个 `newtype`
为何需要 `newtype`Rust 这多如繁星的 Old 类型满足不了我们吗?这是因为:
@ -13,9 +15,11 @@
一箩筐的理由~~ 让我们先从第二点讲起。
#### 为外部类型实现外部特征
在之前的章节中,我们有讲过,如果在外部类型上实现外部特征必须使用 `newtype` 的方式,否则你就得遵循孤儿规则:要为类型 `A` 实现特征 `T`,那么 `A` 或者 `T` 必须至少有一个在当前的作用范围内。
例如,如果想使用 `println!("{}", v)` 的方式去格式化输出一个动态数组 `Vec`,以期给用户提供更加清晰可读的内容,那么就需要为 `Vec` 实现 `Display` 特征,但是这里有一个问题: `Vec` 类型定义在标准库中,`Display` 亦然,这时就可以祭出大杀器 `newtype` 来解决:
```rust
use std::fmt;
@ -36,7 +40,9 @@ fn main() {
如上所示,使用元组结构体语法 `struct Wrapper(Vec<String>)` 创建了一个 `newtype` Wrapper然后为它实现 `Display` 特征,最终实现了对 `Vec` 动态数组的格式化输出。
#### 更好的可读性及类型异化
首先更好的可读性不等于更少的代码如果你学过Scala相信会深有体会其次下面的例子只是一个示例未必能体现出更好的可读性
首先,更好的可读性不等于更少的代码(如果你学过 Scala相信会深有体会其次下面的例子只是一个示例未必能体现出更好的可读性
```rust
use std::ops::Add;
use std::fmt;
@ -66,6 +72,7 @@ fn calculate_distance(d1: Meters, d2: Meters) -> Meters {
```
上面代码创建了一个 `newtype` Meters为其实现 `Display``Add` 特征,接着对两个距离进行求和计算,最终打印出该距离:
```console
目标地点距离你30米
```
@ -73,7 +80,9 @@ fn calculate_distance(d1: Meters, d2: Meters) -> Meters {
事实上,除了可读性外,还有一个极大的优点:如果给 `calculate_distance` 传一个其它的类型,例如 `struct MilliMeters(u32);`,该代码将无法编译。尽管 `Meters``MilliMeters` 都是对 `u32` 类型的简单包装,但是**它们是不同的类型**
#### 隐藏内部类型的细节
众所周知Rust 的类型有很多自定义的方法,假如我们把某个类型传给了用户,但是又不想用户调用这些方法,就可以使用 `newtype`
```rust
struct Meters(u32);
@ -89,15 +98,17 @@ fn main() {
不过需要偷偷告诉你的是,这种方式实际上是掩耳盗铃,因为用户依然可以通过 `n.0.pow(2)` 的方式来调用内部类型的方法 :)
## 类型别名(Type Alias)
除了使用 `newtype`,我们还可以使用一个更传统的方式来创建新类型:类型别名
```rust
type Meters = u32
```
嗯,不得不说,类型别名的方式看起来比 `newtype` 顺眼的多,而且跟其它语言的使用方式几乎一致,但是:
**类型别名并不是一个独立的全新的类型,而是某一个类型的别名**,因此编译器依然会把 `Meters``u32` 来使用:
```rust
type Meters = i32;
@ -108,10 +119,12 @@ println!("x + y = {}", x + y);
```
上面的代码将顺利编译通过,但是如果你使用 `newtype` 模式,该代码将无情报错,简单做个总结:
- 类型别名仅仅是别名,只是为了让可读性更好,并不是全新的类型,`newtype` 才是!
- 类型别名无法实现*为外部类型实现外部特征*等功能,而 `newtype` 可以
类型别名除了让类型可读性更好,还能**减少模版代码的使用**
```rust
let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi"));
@ -127,6 +140,7 @@ fn returns_long_type() -> Box<dyn Fn() + Send + 'static> {
`f` 是一个令人眼花缭乱的类型 `Box<dyn Fn() + Send + 'static>`,如果仔细看,会发现其实只有一个 `Send` 特征不认识,`Send` 是什么在这里不重要,你只需理解,`f` 就是一个 `Box<dyn T>` 类型的特征对象,实现了 `Fn()` 和` Send` 特征,同时生命周期为 `'static`
因为 `f` 的类型贼长,导致了后面我们在使用它时,到处都充斥这些不太优美的类型标注,好在类型别名可解君忧:
```rust
type Thunk = Box<dyn Fn() + Send + 'static>;
@ -148,6 +162,7 @@ Bang是不是立刻大幅简化了我们的使用。喝着奶茶、哼
例如在 `std::io` 库中,它定义了自己的 `Error` 类型:`std::io::Error`,那么如果要使用该 `Result` 就要用这样的语法:`std::result::Result<T, std::io::Error>;`,想象一下代码中充斥着这样的东东是一种什么感受?颤抖吧。。。
由于使用 `std::io` 库时,它的所有错误类型都是 `std::io::Error`,那么我们完全可以把该错误对用户隐藏起来,只在内部使用即可,因此就可以使用类型别名来简化实现:
```rust
type Result<T> = std::result::Result<T, std::io::Error>;
```
@ -157,7 +172,9 @@ Bingo这样一来其它库只需要使用 `std::io::Result<T>` 即可替
更香的是,由于它只是别名,因此我们可以用它来调用真实类型的所有方法,甚至包括 `?` 符号!
## !永不返回类型
在[函数](https://course.rs/basic/base-type/function.html#永不返回的函数)那章,曾经介绍过 `!` 类型:`!` 用来说明一个函数永不返回任何值,当时可能体会不深,没事,在学习了更多手法后,保证你有全新的体验:
```rust
fn main() {
let i = 2;
@ -169,6 +186,7 @@ fn main() {
```
上面函数,会报出一个编译错误:
```console
error[E0308]: `match` arms have incompatible types // match的分支类型不同
--> src/main.rs:5:13
@ -186,6 +204,7 @@ error[E0308]: `match` arms have incompatible types // match的分支类型不同
原因很简单: 要赋值给 `v`,就必须保证 `match` 的各个分支返回的值是同一个类型,但是上面一个分支返回数值、另一个分支返回元类型 `()`,自然会出错。
既然 `println` 不行,那再试试 `panic`
```rust
fn main() {
let i = 2;

@ -1,15 +1,17 @@
# 整数转换为枚举
在 Rust 中,从枚举到整数的转换很容易,但是反过来,就没那么容易,甚至部分实现还挺邪恶, 例如使用`transmute`。
在 Rust 中,从枚举到整数的转换很容易,但是反过来,就没那么容易,甚至部分实现还挺邪恶, 例如使用`transmute`。
## 一个真实场景的需求
在实际场景中,从枚举到整数的转换有时还是非常需要的,例如你有一个枚举类型,然后需要从外面传入一个整数,用于控制后续的流程走向,此时就需要用整数去匹配相应的枚举(你也可以用整数匹配整数-, -,看看会不会被喷)。
既然有了需求,剩下的就是看看该如何实现,这篇文章的水远比你想象的要深,且看八仙过海各显神通。
## C 语言的实现
## C语言的实现
对于 C 语言来说,万物皆邪恶,因此我们不讨论安全,只看实现,不得不说很简洁:
```C
#include <stdio.h>
@ -33,6 +35,7 @@ int main(void)
```
但是在 Rust 中,以下代码:
```rust
enum MyEnum {
A = 1,
@ -57,9 +60,11 @@ fn main() {
就会报错: `MyEnum::A => {} mismatched types, expected i32, found enum MyEnum`
## 使用三方库
首先可以想到的肯定是三方库,毕竟 Rust 的生态目前已经发展的很不错,类似的需求总是有的,这里我们先使用`num-traits`和`num-derive`来试试。
在`Cargo.toml`中引入:
```toml
[dependencies]
num-traits = "0.2.14"
@ -67,8 +72,9 @@ num-derive = "0.3.3"
```
代码如下:
```rust
use num_derive::FromPrimitive;
use num_derive::FromPrimitive;
use num_traits::FromPrimitive;
#[derive(FromPrimitive)]
@ -93,7 +99,9 @@ fn main() {
除了上面的库,还可以使用一个较新的库: [`num_enums`](https://github.com/illicitonion/num_enum)。
## TryFrom + 宏
在 Rust 1.34 后,可以实现`TryFrom`特征来做转换:
```rust
use std::convert::TryFrom;
@ -112,6 +120,7 @@ impl TryFrom<i32> for MyEnum {
```
以上代码定义了从`i32`到`MyEnum`的转换,接着就可以使用`TryInto`来实现转换:
```rust
use std::convert::TryInto;
@ -128,6 +137,7 @@ fn main() {
```
但是上面的代码有个问题,你需要为每个枚举成员都实现一个转换分支,非常麻烦。好在可以使用宏来简化,自动根据枚举的定义来实现`TryFrom`特征:
```rust
macro_rules! back_to_enum {
($(#[$meta:meta])* $vis:vis enum $name:ident {
@ -160,16 +170,13 @@ back_to_enum! {
}
```
## 邪恶之王 std::mem::transmute
## 邪恶之王std::mem::transmute
**这个方法原则上并不推荐,但是有其存在的意义,如果要使用,你需要清晰的知道自己为什么使用**。
在之前的类型转换章节,我们提到过非常邪恶的[`transmute`转换](../basic/converse.md#变形记(Transmutes)),其实,当你知道数值一定不会超过枚举的范围时(例如枚举成员对应123传入的整数也在这个范围内),就可以使用这个方法完成变形。
在之前的类型转换章节,我们提到过非常邪恶的[`transmute`转换](<../basic/converse.md#(Transmutes)>),其实,当你知道数值一定不会超过枚举的范围时(例如枚举成员对应 123传入的整数也在这个范围内),就可以使用这个方法完成变形。
> 最好使用#[repr(..)]来控制底层类型的大小免得本来需要i32结果传入i64最终内存无法对齐产生奇怪的结果
> 最好使用#[repr(..)]来控制底层类型的大小,免得本来需要 i32结果传入 i64最终内存无法对齐产生奇怪的结果
```rust
#[repr(i32)]
@ -193,8 +200,9 @@ fn main() {
既然是邪恶之王,当然得有真本事,无需标准库、也无需 unstable 的 Rust 版本我们就完成了转换awesome!??
## 总结
本文列举了常用(其实差不多也是全部了,还有一个 unstable 特性没提到)的从整数转换为枚举的方式,推荐度按照出现的先后顺序递减。
但是推荐度最低,不代表它就没有出场的机会,只要使用边界清晰,一样可以大放光彩,例如最后的`transmute`函数.
但是推荐度最低,不代表它就没有出场的机会,只要使用边界清晰,一样可以大放光彩,例如最后的`transmute`函数.

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

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

@ -1,17 +1,20 @@
# 深入生命周期
其实关于生命周期的常用特性,在上一节中,我们已经概括得差不多了,本章主要讲解生命周期的一些高级或者不为人知的特性。对于新手,完全可以跳过本节内容,进行下一章节的学习。
## 不太聪明的生命周期检查
在 Rust 语言学习中,一个很重要的部分就是阅读一些你可能不经常遇到,但是一旦遇到就难以理解的代码,这些代码往往最令人头疼的就是生命周期,这里我们就来看看一些本以为可以编译,但是却因为生命周期系统不够聪明导致编译失败的代码。
#### 例子1
#### 例子 1
```rust
#[derive(Debug)]
struct Foo;
impl Foo {
fn mutate_and_share(&mut self) -> &Self {
&*self
&*self
}
fn share(&self) {}
}
@ -27,6 +30,7 @@ fn main() {
上面的代码中,`foo.mutate_and_share()` 虽然借用了 `&mut self`,但是它最终返回的是一个 `&self`,然后赋值给 `loan`,因此理论上来说它最终是进行了不可变借用,同时 `foo.share` 也进行了不可变借用,那么根据 Rust 的借用规则:多个不可变借用可以同时存在,因此该代码应该编译通过。
事实上,运行代码后,你将看到一个错误:
```console
error[E0502]: cannot borrow `foo` as immutable because it is also borrowed as mutable
--> src/main.rs:12:5
@ -42,12 +46,13 @@ error[E0502]: cannot borrow `foo` as immutable because it is also borrowed as mu
编译器的提示在这里其实有些难以理解,因为可变借用仅在 `mutate_and_share` 方法内部有效,出了该方法后,就只有返回的不可变借用,因此,按理来说可变借用不应该在 `main` 的作用范围内存在。
对于这个反直觉的事情,让我们用生命周期来解释下,可能你就很好理解了:
```rust
struct Foo;
impl Foo {
fn mutate_and_share<'a>(&'a mut self) -> &'a Self {
&'a *self
&'a *self
}
fn share<'a>(&'a self) {}
}
@ -74,8 +79,10 @@ fn main() {
上述代码实际上完全是正确的,但是因为生命周期系统的“粗糙实现”,导致了编译错误,目前来说,遇到这种生命周期系统不够聪明导致的编译错误,我们也没有太好的办法,只能修改代码去满足它的需求,并期待以后它会更聪明。
#### 例子2
#### 例子 2
再来看一个例子:
```rust
#![allow(unused)]
fn main() {
@ -121,17 +128,16 @@ error[E0499]: cannot borrow `*map` as mutable more than once at a time
| |_________- returning this value requires that `*map` is borrowed for `'m`
```
分析代码可知在 `match map.get_mut(&key)` 方法调用完成后,对 `map` 的可变借用就可以结束了。但从报错看来,编译器不太聪明,它认为该借用会持续到整个 `match` 语句块的结束(第16行处),这便造成了后续借用的失败。
类似的例子还有很多,由于篇幅有限,就不在这里一一列举,如果大家想要阅读更多的类似代码,可以看看[<<Rust>>](https://github.com/sunface/rust-codes)一书。
分析代码可知在 `match map.get_mut(&key)` 方法调用完成后,对 `map` 的可变借用就可以结束了。但从报错看来,编译器不太聪明,它认为该借用会持续到整个 `match` 语句块的结束(第 16 行处),这便造成了后续借用的失败。
类似的例子还有很多,由于篇幅有限,就不在这里一一列举,如果大家想要阅读更多的类似代码,可以看看[<<Rust >>](https://github.com/sunface/rust-codes)一书。
## 无界生命周期
不安全代码(`unsafe`)经常会凭空产生引用或生命周期,这些生命周期被称为是 **无界(unbound)** 的。
无界生命周期往往是在解引用一个原生指针(裸指针raw pointer)时产生的,换句话说,它是凭空产生的,因为输入参数根本就没有这个生命周期:
无界生命周期往往是在解引用一个原生指针(裸指针 raw pointer)时产生的,换句话说,它是凭空产生的,因为输入参数根本就没有这个生命周期:
```rust
fn f<'a, T>(x: *const T) -> &'a T {
unsafe {
@ -147,10 +153,13 @@ fn f<'a, T>(x: *const T) -> &'a T {
我们在实际应用中,要尽量避免这种无界生命周期。最简单的避免无界生命周期的方式就是在函数声明中运用生命周期消除规则。**若一个输出生命周期被消除了,那么必定因为有一个输入生命周期与之对应**。
## 生命周期约束 HRTB
生命周期约束跟特征约束类似,都是通过形如 `'a: 'b` 的语法,来说明两个生命周期的长短关系。
#### 'a: 'b
假设有两个引用 `&'a i32``&'b i32`,它们的生命周期分别是 `'a``'b`,若 `'a` >= `'b`,则可以定义 `'a:'b`,表示 `'a` 至少要活得跟 `'b` 一样久。
```rust
struct DoubleRef<'a,'b:'a, T> {
r: &'a T,
@ -161,7 +170,9 @@ struct DoubleRef<'a,'b:'a, T> {
例如上述代码定义一个结构体,它拥有两个引用字段,类型都是泛型 `T`,每个引用都拥有自己的生命周期,由于我们使用了生命周期约束 `'b: 'a`,因此 `'b` 必须活得比 `'a` 久,也就是结构体中的 `s` 字段引用的值必须要比 `r` 字段引用的值活得要久。
#### T: 'a
表示类型 `T` 必须比 `'a` 活得要久:
```rust
struct Ref<'a, T: 'a> {
r: &'a T
@ -171,6 +182,7 @@ struct Ref<'a, T: 'a> {
因为结构体字段 `r` 引用了 `T`,因此 `r` 的生命周期 `'a` 必须要比 `T` 的生命周期更短(被引用者的生命周期必须要比引用长)。
在 Rust 1.30 版本之前,该写法是必须的,但是从 1.31 版本开始,编译器可以自动推导 `T: 'a` 类型的约束,因此我们只需这样写即可:
```rust
struct Ref<'a, T> {
r: &'a T
@ -178,6 +190,7 @@ struct Ref<'a, T> {
```
来看一个使用了生命周期约束的综合例子:
```rust
struct ImportantExcerpt<'a> {
part: &'a str,
@ -193,17 +206,19 @@ impl<'a: 'b, 'b> ImportantExcerpt<'a> {
上面的例子中必须添加约束 `'a: 'b` 后,才能成功编译,因为 `self.part` 的生命周期与 `self`的生命周期一致,将 `&'a` 类型的生命周期强行转换为 `&'b` 类型,会报错,只有在 `'a` >= `'b` 的情况下,`'a` 才能转换成 `'b`
## 闭包函数的消除规则
先来看一段简单的代码:
```rust
fn fn_elision(x: &i32) -> &i32 { x }
fn fn_elision(x: &i32) -> &i32 { x }
let closure_slision = |x: &i32| -> &i32 { x };
```
乍一看,这段代码比古天乐还平平无奇,能有什么问题呢?来,拄拐走两圈试试:
```console
error: lifetime may not live long enough
error: lifetime may not live long enough
--> src/main.rs:39:39
|
39 | let closure = |x: &i32| -> &i32 { x }; // fails
@ -220,12 +235,15 @@ error: lifetime may not live long enough
首先给出一个结论:**这个问题,可能很难被解决,建议大家遇到后,还是老老实实用正常的函数,不要秀闭包了**。
对于函数的生命周期而言,它的消除规则之所以能生效是因为它的生命周期完全体现在签名的引用类型上,在函数体中无需任何体现:
```rust
fn fn_elision(x: &i32) -> &i32 {..}
```
因此编译器可以做各种编译优化,也很容易根据参数和返回值进行生命周期的分析,最终得出消除规则。
可是闭包,并没有函数那么简单,它的生命周期分散在参数和闭包函数体中(主要是它没有确切的返回值签名)
```rust
let closure_slision = |x: &i32| -> &i32 { x };
```
@ -234,29 +252,32 @@ let closure_slision = |x: &i32| -> &i32 { x };
由于上述原因(当然,实际情况复杂的多)Rust 语言开发者目前其实是有意针对函数和闭包实现了两种不同的生命周期消除规则。
## NLL (Non-Lexical Lifetime)
之前我们在[引用与借用](../../basic/ownership/borrowing.md#NLL)那一章其实有讲到过这个概念,简单来说就是:**引用的生命周期正常来说应该从借用开始一直持续到作用域结束**,但是这种规则会让多引用共存的情况变得更复杂:
```rust
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// 新编译器中r1,r2作用域在这里结束
let r3 = &mut s;
let r3 = &mut s;
println!("{}", r3);
}
```
按照上述规则,这段代码将会报错,因为 `r1``r2` 的不可变引用将持续到 `main` 函数结束,而在此范围内,我们又借用了 `r3` 的可变引用,这违反了借用的规则:要么多个不可变借用,要么一个可变借用。
按照上述规则,这段代码将会报错,因为 `r1``r2` 的不可变引用将持续到 `main` 函数结束,而在此范围内,我们又借用了 `r3` 的可变引用,这违反了借用的规则:要么多个不可变借用,要么一个可变借用。
好在,该规则从 1.31 版本引入 `NLL` 后,就变成了:**引用的生命周期从借用处开始,一直持续到最后一次使用的地方**。
按照最新的规则,我们再来分析一下上面的代码。`r1` 和 `r2` 不可变借用在 `println!` 后就不再使用,因此生命周期也随之结束,那么 `r3` 的借用就不再违反借用的规则,皆大欢喜。
再来看一段关于 `NLL` 的代码解释:
```rust
let mut u = 0i32;
let mut v = 1i32;
@ -280,13 +301,15 @@ use(a); // | |
在实际项目中,`NLL` 规则可以大幅减少引用冲突的情况,极大的便利了用户,因此广受欢迎,最终该规则甚至演化成一个独立的项目,未来可能会进一步简化我们的使用,`Polonius`
- [项目地址](https://github.com/rust-lang/polonius)
- [项目地址](https://github.com/rust-lang/polonius)
- [具体介绍](http://smallcultfollowing.com/babysteps/blog/2018/04/27/an-alias-based-formulation-of-the-borrow-checker/)
# Reborrow 再借用
学完 `NLL` 后,我们就有了一定的基础,可以继续学习关于借用和生命周期的一个高级内容:**再借用**。
先来看一段代码:
```rust
#[derive(Debug)]
struct Point {
@ -315,13 +338,14 @@ fn main() {
以上代码,大家可能会觉得可变引用 `r` 和不可变引用 `rr` 同时存在会报错吧?但是事实上并不会,原因在于 `rr` 是对 `r` 的再借用。
对于再借用而言,`rr` 再借用时不会破坏借用规则,但是你不能在它的生命周期内再使用原来的借用 `r`,来看看对上段代码的分析:
```rust
fn main() {
let mut p = Point { x: 0, y: 0 };
let r = &mut p;
// reborrow! 此时对`r`的再借用不会导致跟上面的借用冲突
let rr: &Point = &*r;
// 再借用`rr`最后一次使用发生在这里,在它的生命周期中,我们并没有使用原来的借用`r`,因此不会报错
println!("{:?}", rr);
@ -332,41 +356,47 @@ fn main() {
```
再来看一个例子:
```rust
use std::vec::Vec;
fn read_length(strings: &mut Vec<String>) -> usize {
strings.len()
}
```
如上所示,函数体内对参数的二次借用也是典型的 `Reborrow` 场景。
如上所示,函数体内对参数的二次借用也是典型的 `Reborrow` 场景。
那么下面让我们来做件坏事,破坏这条规则,使其报错:
```rust
fn main() {
let mut p = Point { x: 0, y: 0 };
let r = &mut p;
let rr: &Point = &*r;
let rr: &Point = &*r;
r.move_to(10, 10);
println!("{:?}", rr);
println!("{:?}", r);
}
```
果然,破坏永远比重建简单 :) 只需要在 `rr` 再借用的生命周期内使用一次原来的借用 `r` 即可!
## 生命周期消除规则补充
在上一节中,我们介绍了三大基础生命周期消除规则,实际上,随着 Rust 的版本进化,该规则也在不断演进,这里再介绍几个常见的消除规则:
#### impl块消除
#### impl 块消除
```rust
impl<'a> Reader for BufReader<'a> {
// methods go here
// impl内部实际上没有用到'a
}
```
如果你以前写的`impl`块长上面这样,同时在 `impl` 内部的方法中,根本就没有用到 `'a`,那就可以写成下面的代码形式。
```rust
@ -380,6 +410,7 @@ impl Reader for BufReader<'_> {
歪个楼,有读者估计会发问:既然用不到 `'a`,为何还要写出来?如果你仔细回忆下上一节的内容,里面有一句专门用粗体标注的文字:**生命周期参数也是类型的一部分**,因此 `BufReader<'a>` 是一个完整的类型,在实现它的时候,你不能把 `'a` 给丢了!
#### 生命周期约束消除
```rust
// Rust 2015
struct Ref<'a, T: 'a> {
@ -395,6 +426,7 @@ struct Ref<'a, T> {
在本节的生命周期约束中,也提到过,新版本 Rust 中,上面情况中的 `T: 'a` 可以被消除掉当然你也可以显式的声明但是会影响代码可读性。关于类似的场景Rust 团队计划在未来提供更多的消除规则,但是,你懂的,计划未来就等于未知。
## 一个复杂的例子
下面是一个关于生命周期声明过大的例子,会较为复杂,希望大家能细细阅读,它能帮你对生命周期的理解更加深入。
```rust
@ -409,7 +441,7 @@ impl<'a> Interface<'a> {
}
struct Manager<'a> {
text: &'a str
text: &'a str
}
struct List<'a> {
@ -422,7 +454,7 @@ impl<'a> List<'a> {
manager: &mut self.manager
}
}
}
}
fn main() {
let mut list = List {
@ -430,11 +462,11 @@ fn main() {
text: "hello"
}
};
list.get_interface().noop();
println!("Interface should be dropped here and the borrow released");
// 下面的调用会失败,因为同时有不可变/可变借用
// 但是Interface在之前调用完成后就应该被释放了
use_list(&list);
@ -444,7 +476,9 @@ fn use_list(list: &List) {
println!("{}", list.manager.text);
}
```
运行后报错:
```console
error[E0502]: cannot borrow `list` as immutable because it is also borrowed as mutable // `list`无法被借用,因为已经被可变借用
--> src/main.rs:40:14
@ -464,6 +498,7 @@ error[E0502]: cannot borrow `list` as immutable because it is also borrowed as m
这是因为我们在 `get_interface` 方法中声明的 `lifetime` 有问题,该方法的参数的生命周期是 `'a`,而 `List` 的生命周期也是 `'a`,说明该方法至少活得跟 `List` 一样久,再回到 `main` 函数中,`list` 可以活到 `main` 函数的结束,因此 `list.get_interface()` 借用的可变引用也会活到 `main` 函数的结束,在此期间,自然无法再进行借用了。
要解决这个问题,我们需要为 `get_interface` 方法的参数给予一个不同于 `List<'a>` 的生命周期 `'b`,最终代码如下:
```rust
struct Interface<'b, 'a: 'b> {
manager: &'b mut Manager<'a>
@ -476,7 +511,7 @@ impl<'b, 'a: 'b> Interface<'b, 'a> {
}
struct Manager<'a> {
text: &'a str
text: &'a str
}
struct List<'a> {
@ -490,7 +525,7 @@ impl<'a> List<'a> {
manager: &mut self.manager
}
}
}
}
fn main() {
@ -499,11 +534,11 @@ fn main() {
text: "hello"
}
};
list.get_interface().noop();
println!("Interface should be dropped here and the borrow released");
// 下面的调用可以通过因为Interface的生命周期不需要跟list一样长
use_list(&list);
}

@ -8,7 +8,9 @@
Rust 生命周期之所以难,是因为这个概念对于我们来说是全新的,没有其它编程语言的经验可以借鉴。当你觉得难的时候,不用过于担心,这个难对于所有人都是平等的,多点付出就能早点解决此拦路虎,同时本书也会尽力帮助大家减少学习难度(生命周期很可能是 Rust 中最难的部分)。
## 悬垂指针和生命周期
生命周期的主要作用是避免悬垂引用,它会导致程序引用了本不该引用的数据:
```rust
{
let r;
@ -28,6 +30,7 @@ Rust 生命周期之所以难,是因为这个概念对于我们来说是全新
- `r` 引用了内部花括号中的 `x` 变量,但是 `x` 会在内部花括号 `}` 处被释放,因此回到外部花括号后,`r` 会引用一个无效的 `x`
此处 `r` 就是一个悬垂指针,它引用了提前被释放的变量 `x`,可以预料到,这段代码会报错:
```console
error[E0597]: `x` does not live long enough // `x` 活得不够久
--> src/main.rs:7:17
@ -36,15 +39,17 @@ error[E0597]: `x` does not live long enough // `x` 活得不够久
| ^^ borrowed value does not live long enough // 被借用的 `x` 活得不够久
8 | }
| - `x` dropped here while still borrowed // `x` 在这里被丢弃,但是它依然还在被借用
9 |
10 | println!("r: {}", r);
9 |
10 | println!("r: {}", r);
| - borrow later used here // 对 `x` 的借用在此处被使用
```
在这里 `r` 拥有更大的作用域,或者说**活得更久**。如果 Rust 不阻止该垂悬引用的发生,那么当 `x` 被释放后,`r` 所引用的值就不再是合法的,会导致我们程序发生异常行为,且该异常行为有时候会很难被发现。
## 借用检查
为了保证 Rust 的所有权和借用的正确性Rust 使用了一个借用检查器(Borrow checker),来检查我们程序的借用正确性:
```rust
{
let r; // ---------+-- 'a
@ -63,6 +68,7 @@ error[E0597]: `x` does not live long enough // `x` 活得不够久
在编译期Rust 会比较两个变量的生命周期,结果发现 `r` 明明拥有生命周期 `'a`,但是却引用了一个小得多的生命周期 `'b`,在这种情况下,编译器会认为我们的程序存在风险,因此拒绝运行。
如果想要编译通过,也很简单,只要 `'b``'a` 大就好。总之,`x` 变量只要比 `r` 活得久,那么 `r` 就能随意引用 `x` 且不会存在危险:
```rust
{
let x = 5; // ----------+-- 'b
@ -79,7 +85,9 @@ error[E0597]: `x` does not live long enough // `x` 活得不够久
通过之前的内容,我们了解了何为生命周期,也了解了 Rust 如何利用生命周期来确保引用是合法的,下面来看看函数中的生命周期。
## 函数中的生命周期
先来考虑一个例子 - 返回两个字符串切片中较长的那个,该函数的参数是两个字符串切片,返回值也是字符串切片:
```rust
fn main() {
let string1 = String::from("abcd");
@ -101,6 +109,7 @@ fn longest(x: &str, y: &str) -> &str {
```
这段 `longest` 实现,非常标准优美,就连多余的 `return` 和分号都没有,可是现实总是给我们重重一击:
```console
error[E0106]: missing lifetime specifier
--> src/main.rs:9:33
@ -108,7 +117,7 @@ error[E0106]: missing lifetime specifier
9 | fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter // 参数需要一个生命周期
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is
= help: this function's return type contains a borrowed value, but the signature does not say whether it is
borrowed from `x` or `y`
= 帮助: 该函数的返回值是一个引用类型,但是函数签名无法说明,该引用是借用自 `x` 还是 `y`
help: consider introducing a named lifetime parameter // 考虑引入一个生命周期
@ -117,7 +126,7 @@ help: consider introducing a named lifetime parameter // 考虑引入一个生
| ^^^^ ^^^^^^^ ^^^^^^^ ^^^
```
这真是一个复杂的提示那感觉就好像是生命周期去非诚勿扰相亲结果在初印象环节就23盏灯全灭。等等先别急如果你仔细阅读就会发现其实主要是编译器无法知道该函数的返回值到底引用 `x` 还是 `y` **因为编译器需要知道这些,来确保函数调用后的引用生命周期分析**。
喔,这真是一个复杂的提示,那感觉就好像是生命周期去非诚勿扰相亲,结果在初印象环节就 23 盏灯全灭。等等,先别急,如果你仔细阅读,就会发现,其实主要是编译器无法知道该函数的返回值到底引用 `x` 还是 `y` **因为编译器需要知道这些,来确保函数调用后的引用生命周期分析**。
不过说来尴尬,就这个函数而言,我们也不知道返回值到底引用哪个,因为一个分支返回 `x`,另一个分支返回 `y`...这可咋办?先来分析下。
@ -126,6 +135,7 @@ help: consider introducing a named lifetime parameter // 考虑引入一个生
因此,这时就回到了文章开头说的内容:在存在多个引用时,编译器有时会无法自动推导生命周期,此时就需要我们手动去标注,通过为参数标注合适的生命周期来帮助编译器进行借用检查的分析。
## 生命周期标注语法
> 生命周期标注并不会改变任何引用的实际作用域 -- 鲁迅
鲁迅说过的话,总是值得重点标注,当你未来更加理解生命周期时,你才会发现这句话的精髓和重要!现在先简单记住,**标记的生命周期只是为了取悦编译器,让编译器不要难为我们**,记住了吗?没记住,再回头看一遍,这对未来你遇到生命周期问题时会有很大的帮助!
@ -135,19 +145,23 @@ help: consider introducing a named lifetime parameter // 考虑引入一个生
例如一个变量,只能活一个花括号,那么就算你给它标注一个活全局的生命周期,它还是会在前面的花括号结束处被释放掉,并不会真的全局存活。
生命周期的语法也颇为与众不同,以 `'` 开头,名称往往是一个单独的小写字母,大多数人都用 `'a` 来作为生命周期的名称。 如果是引用类型的参数,那么生命周期会位于引用符号 `&` 之后,并用一个空格来将生命周期和引用参数分隔开:
```rust
&i32 // 一个引用
&'a i32 // 具有显式生命周期的引用
&'a mut i32 // 具有显式生命周期的可变引用
```
一个生命周期标注,它自身并不具有什么意义,因为生命周期的作用就是告诉编译器多个引用之间的关系。例如,有一个函数,它的第一个参数 `first` 是一个指向 `i32` 类型的引用,具有生命周期 `'a`,该函数还有另一个参数 `second`,它也是指向 `i32` 类型的引用,并且同样具有生命周期 `'a`。此处生命周期标注仅仅说明,**这两个参数 `first``second` 至少活得和'a一样久至于到底活多久或者哪个活得更久抱歉我们都无法得知**
一个生命周期标注,它自身并不具有什么意义,因为生命周期的作用就是告诉编译器多个引用之间的关系。例如,有一个函数,它的第一个参数 `first` 是一个指向 `i32` 类型的引用,具有生命周期 `'a`,该函数还有另一个参数 `second`,它也是指向 `i32` 类型的引用,并且同样具有生命周期 `'a`。此处生命周期标注仅仅说明,**这两个参数 `first``second` 至少活得和'a 一样久,至于到底活多久或者哪个活得更久,抱歉我们都无法得知**
```rust
fn useless<'a>(first: &'a i32, second: &'a i32) {}
```
#### 函数签名中的生命周期标注
继续之前的 `longest` 函数,从两个字符串切片中返回较长的那个:
```rust
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
@ -171,7 +185,8 @@ fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
当把具体的引用传给 `longest` 时,那生命周期 `'a` 的大小就是 `x``y` 的作用域的重合部分,换句话说,`'a` 的大小将等于 `x``y` 中较小的那个。由于返回值的生命周期也被标记为 `'a`,因此返回值的生命周期也是 `x``y` 中作用域较小的那个。
说实话,这段文字我写的都快崩溃了,不知道你们读起来如何,实在***太绕了。。那就干脆用一个例子来解释吧:
说实话,这段文字我写的都快崩溃了,不知道你们读起来如何,实在\*\*\*太绕了。。那就干脆用一个例子来解释吧:
```rust
fn main() {
let string1 = String::from("long string is long");
@ -191,6 +206,7 @@ fn main() {
因此,在这种情况下,通过生命周期标注,编译器得出了和我们肉眼观察一样的结论,而不再是一个蒙圈的大聪明。
再来看一个例子,该例子证明了 `result` 的生命周期必须等于两个参数中生命周期较小的那个:
```rust
fn main() {
let string1 = String::from("long string is long");
@ -204,13 +220,14 @@ fn main() {
```
Bang错误冒头了
```console
error[E0597]: `string2` does not live long enough
--> src/main.rs:6:44
|
6 | result = longest(string1.as_str(), string2.as_str());
| ^^^^^^^ borrowed value does not live long enough
7 | }
| ^^^^^^^ borrowed value does not live long enough
7 | }
| - `string2` dropped here while still borrowed
8 | println!("The longest string is {}", result);
| ------ borrow later used here
@ -227,7 +244,9 @@ error[E0597]: `string2` does not live long enough
小练习:尝试着去更改 `longest` 函数,例如修改参数、生命周期或者返回值,然后推测结果如何,最后再跟编译器的输出进行印证。
#### 深入思考生命周期标注
使用生命周期的方式往往取决于函数的功能,例如之前的 `longest` 函数,如果它永远只返回第一个参数 `x`,生命周期的标注该如何修改(该例子就是上面的小练习结果之一)?
```rust
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
@ -237,10 +256,12 @@ fn longest<'a>(x: &'a str, y: &str) -> &'a str {
在此例中,`y` 完全没有被使用,因此 `y` 的生命周期与 `x` 和返回值的生命周期没有任何关系,意味着我们也不必再为 `y` 标注生命周期,只需要标注 `x` 参数和返回值即可。
**函数的返回值如果是一个引用类型,那么它的生命周期只会来源于**
- 函数参数的生命周期
- 函数体中某个新建引用的生命周期
若是后者情况,就是典型的悬垂引用场景:
```rust
fn longest<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("really long string");
@ -249,6 +270,7 @@ fn longest<'a>(x: &str, y: &str) -> &'a str {
```
上面的函数的返回值就和参数 `x``y` 没有任何关系,而是引用了函数体内创建的字符串,那么很显然,该函数会报错:
```console
error[E0515]: cannot return value referencing local variable `result` // 返回值result引用了本地的变量
--> src/main.rs:11:5
@ -263,6 +285,7 @@ error[E0515]: cannot return value referencing local variable `result` // 返回
主要问题就在于,`result` 在函数结束后就被释放,但是在函数结束后,对 `result` 的引用依然在继续。在这种情况下,没有办法指定合适的生命周期来让编译通过,因此我们也就在 Rust 中避免了悬垂引用。
那遇到这种情况该怎么办?最好的办法就是返回内部字符串的所有权,然后把字符串的所有权转移给调用者:
```rust
fn longest<'a>(_x: &str, _y: &str) -> String {
String::from("really long string")
@ -273,12 +296,14 @@ fn main() {
}
```
至此可以对生命周期进行下总结生命周期语法用来将函数的多个引用参数和返回值的作用域关联到一起一旦关联到一起后Rust就拥有充分的信息来确保我们的操作是内存安全的。
至此可以对生命周期进行下总结生命周期语法用来将函数的多个引用参数和返回值的作用域关联到一起一旦关联到一起后Rust 就拥有充分的信息来确保我们的操作是内存安全的。
## 结构体中的生命周期
不仅仅函数具有生命周期,结构体其实也有这个概念,只不过我们之前对结构体的使用都停留在非引用类型字段上。细心的同学应该能回想起来,之前为什么不在结构体中使用字符串字面量或者字符串切片,而是统一使用 `String` 类型?原因很简单,后者在结构体初始化时,只要转移所有权即可,而前者,抱歉,它们是引用,它们不能为所欲为。
既然之前已经理解了生命周期,那么意味着在结构体中使用引用也变得可能:只要为结构体中的**每一个引用标注上生命周期**即可:
```rust
struct ImportantExcerpt<'a> {
part: &'a str,
@ -295,9 +320,10 @@ fn main() {
`ImportantExcerpt` 结构体中有一个引用类型的字段 `part`,因此需要为它标注上生命周期。结构体的生命周期标注语法跟泛型参数语法很像,需要对生命周期参数进行声明 `<'a>`。该生命周期标注说明,**结构体 `ImportantExcerpt` 所引用的字符串 `str` 必须比该结构体活得更久**。
`main` 函数实现来看,`ImportantExcerpt` 的生命周期从第4行开始`main` 函数末尾结束,而该结构体引用的字符串从第一行开始,也是到 `main` 函数末尾结束,可以得出结论**结构体引用的字符串活得比结构体久**,这符合了编译器对生命周期的要求,因此编译通过。
`main` 函数实现来看,`ImportantExcerpt` 的生命周期从第 4 行开始,到 `main` 函数末尾结束,而该结构体引用的字符串从第一行开始,也是到 `main` 函数末尾结束,可以得出结论**结构体引用的字符串活得比结构体久**,这符合了编译器对生命周期的要求,因此编译通过。
与之相反,下面的代码就无法通过编译:
```rust
#[derive(Debug)]
struct ImportantExcerpt<'a> {
@ -318,6 +344,7 @@ fn main() {
```
观察代码,**可以看出结构体比它引用的字符串活得更久**,引用字符串在内部语句块末尾 `}` 被释放后,`println!` 依然在外面使用了该结构体,因此会导致无效的引用,不出所料,编译报错:
```console
error[E0597]: `novel` does not live long enough
--> src/main.rs:10:30
@ -332,7 +359,9 @@ error[E0597]: `novel` does not live long enough
```
## 生命周期消除
实际上,对于编译器来说,每一个引用类型都有一个生命周期,那么为什么我们在使用过程中,很多时候无需标注生命周期?例如:
```rust
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
@ -357,11 +386,12 @@ fn first_word(s: &str) -> &str {
如果是后者,就会出现悬垂引用,最终被编译器拒绝,因此只剩一种情况:返回值的引用是获取自参数,这就意味着参数和返回值的生命周期是一样的。道理很简单,我们能看出来,编译器自然也能看出来,因此,就算我们不标注生命周期,也不会产生歧义。
实际上,在 Rust 1.0 版本之前,这种代码果断不给通过,因为 Rust 要求必须显式的为所有引用标注生命周期:
```rust
fn first_word<'a>(s: &'a str) -> &'a str {
```
在写了大量的类似代码后Rust社区抱怨声四起包括开发者自己都忍不了了最终揭锅而起这才有了我们今日的幸福。
在写了大量的类似代码后Rust 社区抱怨声四起,包括开发者自己都忍不了了,最终揭锅而起,这才有了我们今日的幸福。
生命周期消除的规则不是一蹴而就,而是伴随着 `总结-改善` 流程的周而复始一步一步走到今天这也意味着该规则以后可能也会进一步增加我们需要手动标注生命周期的时候也会越来越少hooray!
@ -371,54 +401,60 @@ fn first_word<'a>(s: &'a str) -> &'a str {
- **函数或者方法中,参数的生命周期被称为 `输入生命周期`,返回值的生命周期被称为 `输出生命周期`**
#### 三条消除规则
编译器使用三条消除规则来确定哪些场景不需要显式地去标注生命周期。其中第一条规则应用在输入生命周期上,第二、三条应用在输出生命周期上。若编译器发现三条规则都不适用时,就会报错,提示你需要手动标注生命周期。
1. **每一个引用参数都会获得独自的生命周期**
例如一个引用参数的函数就有一个生命周期标注: `fn foo<'a>(x: &'a i32)`,两个引用参数的有两个生命周期标注:`fn foo<'a, 'b>(x: &'a i32, y: &'b i32)`, 依此类推。
例如一个引用参数的函数就有一个生命周期标注: `fn foo<'a>(x: &'a i32)`,两个引用参数的有两个生命周期标注:`fn foo<'a, 'b>(x: &'a i32, y: &'b i32)`, 依此类推。
2. **若只有一个输入生命周期(函数参数中只有一个引用类型),那么该生命周期会被赋给所有的输出生命周期**,也就是所有返回值的生命周期都等于该输入生命周期
例如函数 `fn foo(x: &i32) -> &i32``x` 参数的生命周期会被自动赋给返回值 `&i32`,因此该函数等同于 `fn foo<'a>(x: &'a i32) -> &'a i32`
例如函数 `fn foo(x: &i32) -> &i32``x` 参数的生命周期会被自动赋给返回值 `&i32`,因此该函数等同于 `fn foo<'a>(x: &'a i32) -> &'a i32`
3. **若存在多个输入生命周期,且其中一个是 `&self``&mut self`,则 `&self` 的生命周期被赋给所有的输出生命周期**
拥有 `&self` 形式的参数,说明该函数是一个 `方法`,该规则让方法的使用便利度大幅提升。
拥有 `&self` 形式的参数,说明该函数是一个 `方法`,该规则让方法的使用便利度大幅提升。
规则其实很好理解,但是,爱思考的读者肯定要发问了,例如第三条规则,若一个方法,它的返回值的生命周期就是跟参数 `&self` 的不一样怎么办?总不能强迫我返回的值总是和 `&self` 活得一样久吧?! 问得好,答案很简单:手动标注生命周期,因为这些规则只是编译器发现你没有标注生命周期时默认去使用的,当你标注生命周期后,编译器自然会乖乖听你的话。
让我们假装自己是编译器,然后看下以下的函数该如何应用这些规则:
**例子1**
**例子 1**
```rust
fn first_word(s: &str) -> &str { // 实际项目中的手写代码
```
首先,我们手写的代码如上所示时,编译器会先应用第一条规则,为每个参数标注一个生命周期:
```rust
fn first_word<'a>(s: &'a str) -> &str { // 编译器自动为参数添加生命周期
```
此时,第二条规则就可以进行应用,因为函数只有一个输入生命周期,因此该生命周期会被赋予所有的输出生命周期:
```rust
fn first_word<'a>(s: &'a str) -> &'a str { // 编译器自动为返回值添加生命周期
```
此时,编译器为函数签名中的所有引用都自动添加了具体的生命周期,因此编译通过,且用户无需手动去标注生命周期,只要按照 `fn first_word(s: &str) -> &str { ` 的形式写代码即可。
**例子2**
**例子 2**
再来看一个例子:
```rust
fn longest(x: &str, y: &str) -> &str { // 实际项目中的手写代码
```
首先,编译器会应用第一条规则,为每个参数都标注生命周期:
```rust
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
```
但是此时,第二条规则却无法被使用,因为输入生命周期有两个,第三条规则也不符合,因为它是函数,不是方法,因此没有 `&self` 参数。在套用所有规则后,编译器依然无法为返回值标注合适的生命周期,因此,编译器就会报错,提示我们需要手动标注生命周期:
```console
error[E0106]: missing lifetime specifier
--> src/main.rs:1:47
@ -441,7 +477,9 @@ help: consider using one of the available lifetimes here
不得不说Rust 编译器真的很强大,还贴心的给我们提示了该如何修改,虽然。。。好像。。。。它的提示貌似不太准确。这里我们更希望参数和返回值都是 `'a` 命周期。
## 方法中的生命周期
先来回忆下泛型的语法:
```rust
struct Point<T> {
x: T,
@ -456,6 +494,7 @@ impl<T> Point<T> {
```
实际上,为具有生命周期的结构体实现方法时,我们使用的语法跟泛型参数语法很相似:
```rust
struct ImportantExcerpt<'a> {
part: &'a str,
@ -474,6 +513,7 @@ impl<'a> ImportantExcerpt<'a> {
- 方法签名中,往往不需要标注生命周期,得益于生命周期消除的第一和第三规则
下面的例子展示了第三规则应用的场景:
```rust
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
@ -484,6 +524,7 @@ impl<'a> ImportantExcerpt<'a> {
```
首先,编译器应用第一规则,给予每个输入参数一个生命周期:
```rust
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part<'b>(&'a self, announcement: &'b str) -> &str {
@ -493,9 +534,10 @@ impl<'a> ImportantExcerpt<'a> {
}
```
需要注意的是,编译器不知道 `announcement` 的生命周期到底多长,因此它无法简单的给予它生命周期 `'a`,而是重新声明了一个全新的生命周期 `'b`
需要注意的是,编译器不知道 `announcement` 的生命周期到底多长,因此它无法简单的给予它生命周期 `'a`,而是重新声明了一个全新的生命周期 `'b`
接着,编译器应用第三规则,将 `&self` 的生命周期赋给返回值 `&str`
```rust
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part<'b>(&'a self, announcement: &'b str) -> &'a str {
@ -508,6 +550,7 @@ impl<'a> ImportantExcerpt<'a> {
Bingo最开始的代码尽管我们没有给方法标注生命周期但是在第一和第三规则的配合下编译器依然完美的为我们亮起了绿灯。
在结束这块儿内容之前,再来做一个有趣的修改,将方法返回的生命周期改为`'b`
```rust
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part<'b>(&'a self, announcement: &'b str) -> &'b str {
@ -520,6 +563,7 @@ impl<'a> ImportantExcerpt<'a> {
此时,编译器会报错,因为编译器无法知道 `'a``'b` 的关系。 `&self` 生命周期是 `'a`,那么 `self.part` 的生命周期也是 `'a`,但是好巧不巧的是,我们手动为返回值 `self.part` 标注了生命周期 `'b`,因此编译器需要知道 `'a``'b` 的关系。
有一点很容易推理出来:由于 `&'a self` 是被引用的一方,因此引用它的 `&'b str` 必须要活得比它短,否则会出现悬垂引用。因此说明生命周期 `'b` 必须要比 `'a` 小,只要满足了这一点,编译器就不会再报错:
```rust
impl<'a: 'b, 'b> ImportantExcerpt<'a> {
fn announce_and_return_part(&'a self, announcement: &'b str) -> &'b str {
@ -539,16 +583,18 @@ Bang一个复杂的玩意儿被甩到了你面前就问怕不怕
总之,实现方法比想象中简单:加一个约束,就能暗示编译器,尽管引用吧,反正我想引用的内容比我活得久,爱咋咋地,我怎么都不会引用到无效的内容!
## 静态生命周期
在 Rust 中有一个非常特殊的生命周期,那就是 `'static`,拥有该生命周期的引用可以和整个程序活得一样久。
在之前我们学过字符串字面量,提到过它是被硬编码进 Rust 的二进制文件中,因此这些字符串变量全部具有 `'static` 的生命周期:
```rust
let s: &'static str = "我没啥优点,就是活得久,嘿嘿";
```
这时候,有些聪明的小脑瓜就开始开动了:当生命周期不知道怎么标时,对类型施加一个静态生命周期的约束 `T: 'static` 是不是很爽?这样我和编译器再也不用操心它到底活多久了。
嗯,只能说,这个想法是对的,在不少情况下,`'static` 约束确实可以解决生命周期编译不通过的问题但是问题来了本来该引用没有活那么久但是你非要说它活那么久万一引入了潜在的BUG怎么办
嗯,只能说,这个想法是对的,在不少情况下,`'static` 约束确实可以解决生命周期编译不通过的问题,但是问题来了:本来该引用没有活那么久,但是你非要说它活那么久,万一引入了潜在的 BUG 怎么办?
因此,遇到因为生命周期导致的编译不通过问题,首先想的应该是:是否是我们试图创建一个悬垂引用,或者是试图匹配不一致的生命周期,而不是简单粗暴的用 `'static` 来解决问题。
@ -562,7 +608,9 @@ let s: &'static str = "我没啥优点,就是活得久,嘿嘿";
> 事实上,关于 `'static`, 有两种用法: `&'static``T: 'static`,详细内容请参见[此处](https://course.rs/advance/lifetime/static.html)
## 一个复杂例子: 泛型、特征约束
手指已经疲软无力,我好想停止,但是华丽的开场都要有与之匹配的谢幕,那我们就用一个稍微复杂点的例子来结束:
```rust
use std::fmt::Display;
@ -586,7 +634,8 @@ where
依然是熟悉的配方 `longest`,但是多了一段废话: `ann`,因为要用格式化 `{}` 来输出 `ann`,因此需要它实现 `Display` 特征。
## 总结
我不知道支撑我一口气写完的勇气是什么,也许是不做完不爽夫斯基,也许是一些读者对本书的期待,不管如何,这章足足写了 17000 字,可惜不是写小说,不然肯定可以获取很多月票 :)
我不知道支撑我一口气写完的勇气是什么,也许是不做完不爽夫斯基,也许是一些读者对本书的期待,不管如何,这章足足写了 17000 字,可惜不是写小说,不然肯定可以获取很多月票 :)
从本章开始,最大的收获就是可以在结构体中使用引用类型了,说实话,为了引入这个特性,我已经憋了足足 30 章节……

@ -1,9 +1,11 @@
# &'static 和 T: 'static
Rust 的难点之一就在于它有不少容易混淆的概念,例如 `&str` 、`str` 与 `String` 再比如本文标题那两位。不过与字符串也有不同,这两位对于普通用户来说往往是无需进行区分的,但是当大家想要深入学习或使用 Rust 时,它们就会成为成功路上的拦路虎了。
与生命周期的其它章节不同本文短小精悍阅读过程可谓相当轻松愉快话不多说let's go。
`'static` 在 Rust 中是相当常见的,例如字符串字面值就具有 `'static` 生命周期:
```rust
fn main() {
let mark_twain: &str = "Samuel Clemens";
@ -17,6 +19,7 @@ fn print_author(author: &'static str) {
除此之外,特征对象的生命周期也是 `'static`,例如[这里](https://course.rs/fight-with-compiler/lifetime/closure-with-static.html#特征对象的生命周期)所提到的。
除了 `&'static` 的用法外,我们在另外一种场景中也可以见到 `'static` 的使用:
```rust
use std::fmt::Display;
fn main() {
@ -29,14 +32,16 @@ fn print<T: Display + 'static>(message: &T) {
}
```
在这里,很明显 `'static` 是作为生命周期约束来使用了。 **那么问题来了, `&'static``T: 'static` 的用法到底有何区别?**
在这里,很明显 `'static` 是作为生命周期约束来使用了。 **那么问题来了, `&'static``T: 'static` 的用法到底有何区别?**
## `&'static`
`&'static` 对于生命周期有着非常强的要求:一个引用必须要活得跟剩下的程序一样久,才能被标注为 `&'static`
对于字符串字面量来说,它直接被打包到二进制文件中,永远不会被 `drop`,因此它能跟程序活得一样久,自然它的生命周期是 `'static`
但是,**`&'static` 生命周期针对的仅仅是引用,而不是持有该引用的变量,对于变量来说,还是要遵循相应的作用域规则** :
```rust
use std::{slice::from_raw_parts, str::from_utf8_unchecked};
@ -69,11 +74,14 @@ fn main() {
```
上面代码有两点值得注意:
- `&'static` 的引用确实可以和程序活得一样久,因为我们通过 `get_str_at_location` 函数直接取到了对应的字符串
- 持有 `&'static` 引用的变量,它的生命周期受到作用域的限制,大家务必不要搞混了
## `T: 'static`
相比起来,我们的生命周期约束就弱得多了,它只能试图向编译器表达:如果可以的话,我想要一个可以一直存活的变量, see ? 跟 `&'static` 表达的强度完全不一样,下面用例子来说明:
```rust
use std::fmt::Display;
@ -87,7 +95,7 @@ fn main() {
r2 = x;
// r1 和 r2 持有的数据都是 'static 的,因此在花括号结束后,并不会被释放
}
println!("&'static i32: {}", r1); // -> 42
println!("&'static str: {}", r2); // -> &'static str
@ -98,11 +106,11 @@ fn main() {
// s1 虽然没有 'static 生命周期,但是它依然可以满足 T: 'static 的约束
// 充分说明这个约束是多么的弱。。
static_bound(&s1);
static_bound(&s1);
// s1 是 String 类型,没有 'static 的生命周期,因此下面代码会报错
r3 = &s1;
r3 = &s1;
// s1 在这里被 drop
}
println!("{}", r3);
@ -119,15 +127,17 @@ fn static_bound<T: Display + 'static>(t: &T) {
- `T: 'static` 的约束真的很弱,`s1` 明明生命周期只在内部语句块内有效,但是该约束依然可以满足,`static_bound` 成功被调用
## 两者的区别
总之, `&'static` != `T: 'static` ,虽然它们看起来真的非常像。
为了进一步验证,我们修改下 `static_bound` 的签名 :
```rust
use std::fmt::Display;
fn main() {
let s1 = "String".to_string();
static_bound(&s1);
}
@ -137,6 +147,7 @@ fn static_bound<T: Display>(t: &'static T) {
```
在这里,不再使用生命周期约束来限制 `T`,而直接指定 `T` 的生命周期是 `&'static` ,不出所料,代码报错了:
```console
error[E0597]: `s1` does not live long enough
--> src/main.rs:8:18
@ -153,6 +164,7 @@ error[E0597]: `s1` does not live long enough
原因很简单,`s1` 活得不够久,没有满足 `'static` 的生命周期要求。
## 使用经验
至此,相信大家对于 `'static``T: 'static` 也有了清晰的理解,那么我们应该如何使用它们呢?
作为经验之谈,可以这么来:
@ -161,4 +173,3 @@ error[E0597]: `s1` does not live long enough
- 如果你希望满足和取悦编译器,那就使用 `T: 'static`,很多时候它都能解决问题
> 一个小知识,在 Rust 标准库中,有 48 处用到了 &'static 112 处用到了 `T: 'static` ,看来取悦编译器不仅仅是菜鸟需要的,高手也经常用到 :)

@ -1,4 +1,5 @@
# Macro宏编程
# Macro 宏编程
在编程世界可以说是谈“宏”色变,原因在于 C 语言中的宏是非常危险的东东,但并不是所有语言都像 C 这样,例如对于古老的语言 Lisp 来说,宏就是就是一个非常强大的好帮手。
那话说回来,在 Rust 中宏到底是好是坏呢?本章将带你揭开它的神秘面纱。
@ -6,6 +7,7 @@
事实上,我们虽然没有见过宏,但是已经多次用过它,例如在全书的第一个例子中就用到了:`println!("你好,世界")`,这里 `println!` 就是一个最常用的宏,可以看到它和函数最大的区别是:它在调用时多了一个 `!`,除此之外还有 `vec!` 、`assert_eq!` 都是相当常用的,可以说**宏在 Rust 中无处不在**。
细心的读者可能会注意到 `println!` 后面跟着的是 `()`,而 `vec!` 后面跟着的是 `[]`,这是因为宏的参数可以使用 `()`、`[]` 以及 `{}`:
```rust
fn main() {
println!("aaaa");
@ -16,7 +18,7 @@ fn main() {
虽然三种使用形式皆可,但是 Rust 内置的宏都有自己约定俗成的使用方式,例如 `vec![...]`、`assert_eq!(...)` 等。
在 Rust 中宏分为两大类:**声明式宏( *declarative macros* )** `macro_rules!` 和三种**过程宏( *procedural macros* )**:
在 Rust 中宏分为两大类:**声明式宏( _declarative macros_ )** `macro_rules!` 和三种**过程宏( _procedural macros_ )**:
- `#[derive]`,在之前多次见到的派生宏,可以为目标结构体或枚举派生指定的代码,例如 `Debug` 特征
- 类属性宏(Attribute-like macro),用于为目标添加自定义的属性
@ -25,9 +27,11 @@ fn main() {
如果感觉难以理解,也不必担心,接下来我们将逐个看看它们的庐山真面目,在此之前,先来看下为何需要宏,特别是 Rust 的函数明明已经很强大了。
## 宏和函数的区别
宏和函数的区别并不少,而且对于宏擅长的领域,函数其实是有些无能为力的。
#### 元编程
从根本上来说,宏是通过一种代码来生成另一种代码,如果大家熟悉元编程,就会发现两者的共同点。
在[附录 D](https://course.rs/appendix/derive.html)中讲到的 `derive` 属性,就会自动为结构体派生出相应特征所需的代码,例如 `#[derive(Debug)]`,还有熟悉的 `println!``vec!`,所有的这些宏都会展开成相应的代码,且很可能是长得多的代码。
@ -35,22 +39,27 @@ fn main() {
总之,元编程可以帮我们减少所需编写的代码,也可以一定程度上减少维护的成本,虽然函数复用也有类似的作用,但是宏依然拥有自己独特的优势。
#### 可变参数
Rust 的函数签名是固定的:定义了两个参数,就必须传入两个参数,多一个少一个都不行,对于从 JS/TS 过来的同学,这一点其实是有些恼人的。
而宏就可以拥有可变数量的参数,例如可以调用一个参数的 `println!("hello")`,也可以调用两个参数的 `println!("hello {}", name)`
#### 宏展开
由于宏会被展开成其它代码,且这个展开过程是发生在编译器对代码进行解释之前。因此,宏可以为指定的类型实现某个特征:先将宏展开成实现特征的代码后,再被编译。
而函数就做不到这一点,因为它直到运行时才能被调用,而特征需要在编译期被实现。
#### 宏的缺点
相对函数来说,由于宏是基于代码再展开成代码,因此实现相比函数来说会更加复杂,再加上宏的语法更为复杂,最终导致定义宏的代码相当地难读,也难以理解和维护。
## 声明式宏 `macro_rules!`
在 Rust 中使用最广的就是声明式宏,它们也有一些其它的称呼,例如示例宏( macros by example )、`macro_rules!` 或干脆直接称呼为**宏**。
声明式宏允许我们写出类似 `match` 的代码。`match` 表达式是一个控制结构,其接收一个表达式,然后将表达式的结果与多个模式进行匹配,一旦匹配了某个模式,则该模式相关联的代码将被执行:
```rust
match target {
模式1 => 表达式1,
@ -66,7 +75,9 @@ match target {
而**宏也是将一个值跟对应的模式进行匹配,且该模式会与特定的代码相关联**。但是与 `match` 不同的是,**宏里的值是一段 Rust 源代码**(字面量),模式用于跟这段源代码的结构相比较,一旦匹配,传入宏的那段源代码将被模式关联的代码所替换,最终实现宏展开。值得注意的是,**所有的这些都是在编译期发生,并没有运行期的性能损耗**。
#### 简化版的 vec!
在[动态数组 Vector 章节](https://course.rs/basic/collections/vector.html#vec)中,我们学习了使用 `vec!` 来便捷的初始化一个动态数组:
```rust
let v: Vec<u32> = vec![1, 2, 3];
```
@ -74,6 +85,7 @@ let v: Vec<u32> = vec![1, 2, 3];
最重要的是,通过 `vec!` 创建的动态数组支持任何元素类型,也并没有限制数组的长度,如果使用函数,我们是无法做到这一点的。
好在我们有 `macro_rules!`,来看看该如何使用它来实现 `vec!`,以下是一个简化实现:
```rust
#[macro_export]
macro_rules! vec {
@ -102,6 +114,7 @@ macro_rules! vec {
虽然宏和 `match` 都称之为模式,但是前者跟[后者](https://course.rs/basic/match-pattern/all-patterns.html)的模式规则是不同的。如果大家想要更深入的了解宏的模式,可以查看[这里](https://doc.rust-lang.org/reference/macros-by-example.html)。
#### 模式解析
而现在,我们先来简单讲解下 `( $( $x:expr ),* )` 的含义。
首先,我们使用圆括号 `()` 将整个宏模式包裹其中。紧随其后的是 `$()`,跟括号中模式相匹配的值(传入的 Rust 源代码)会被捕获,然后用于代码替换。在这里,模式 `$x:expr` 会匹配任何 Rust 表达式并给予该模式一个名称:`$x`。
@ -116,6 +129,7 @@ macro_rules! vec {
4. `*` 说明之前的模式可以出现零次也可以任意次,这里出现了三次
接下来,我们再来看看与模式相关联、在 `=>` 之后的代码:
```rust
{
{
@ -129,6 +143,7 @@ macro_rules! vec {
```
这里就比较好理解了,`$()` 中的 `temp_vec.push()` 将根据模式匹配的次数生成对应的代码,当调用 `vec![1, 2, 3]` 时,下面这段生成的代码将替代传入的源代码,也就是替代 `vec![1, 2, 3]` :
```rust
{
let mut temp_vec = Vec::new();
@ -140,6 +155,7 @@ macro_rules! vec {
```
如果是 `let v = vec![1, 2, 3]`,那生成的代码最后返回的值 `temp_vec` 将被赋予给变量 `v`,等同于 :
```rust
let v = {
let mut temp_vec = Vec::new();
@ -153,12 +169,14 @@ let v = {
至此,我们定义了一个宏,它可以接受任意类型和数量的参数,并且理解了其语法的含义。
#### 未来将被替代的 `macro_rules`
对于 `macro_rules` 来说它是存在一些问题的因此Rust 计划在未来使用新的声明式宏来替换它:工作方式类似,但是解决了目前存在的一些问题,在那之后,`macro_rules` 将变为 `deprecated` 状态。
由于绝大多数 Rust 开发者都是宏的用户而不是编写者,因此在这里我们不会对 `macro_rules` 进行更深入的学习,如果大家感兴趣,可以看看这本书 [ “The Little Book of Rust Macros”](https://veykril.github.io/tlborm/)。
## 用过程宏为属性标记生成代码
第二种常用的宏就是[*过程宏*](https://doc.rust-lang.org/reference/procedural-macros.html) ( *procedural macros* ),从形式上来看,过程宏跟函数较为相像,但过程宏是使用源代码作为输入参数,基于代码进行一系列操作后,再输出一段全新的代码。**注意过程宏中的derive宏输出的代码并不会替换之前的代码这一点与声明宏有很大的不同**
第二种常用的宏就是[_过程宏_](https://doc.rust-lang.org/reference/procedural-macros.html) ( _procedural macros_ ),从形式上来看,过程宏跟函数较为相像,但过程宏是使用源代码作为输入参数,基于代码进行一系列操作后,再输出一段全新的代码。**注意,过程宏中的 derive 宏输出的代码并不会替换之前的代码,这一点与声明宏有很大的不同!**
至于前文提到的过程宏的三种类型(自定义 `derive`、属性宏、函数宏),它们的工作方式都是类似的。
@ -167,6 +185,7 @@ let v = {
> 事实上,根据[这个说法](https://www.reddit.com/r/rust/comments/t1oa1e/what_are_the_complex_technical_reasons_why/),过程宏放入独立包的原因在于它必须先被编译后才能使用,如果过程宏和使用它的代码在一个包,就必须先单独对过程宏的代码进行编译,然后再对我们的代码进行编译,但悲剧的是 Rust 的编译单元是包,因此你无法做到这一点。
假设我们要创建一个 `derive` 类型的过程宏:
```rust
use proc_macro;
@ -180,12 +199,14 @@ pub fn some_name(input: TokenStream) -> TokenStream {
在理解了过程宏的基本定义后,我们再来看看该如何创建三种类型的过程宏,首先,从大家最熟悉的 `derive` 开始。
## 自定义 `derive` 过程宏
假设我们有一个特征 `HelloMacro`,现在有两种方式让用户使用它:
- 为每个类型手动实现该特征,就像之前[特征章节](https://course.rs/basic/trait/trait.html#为类型实现特征)所做的
- 使用过程宏来统一实现该特征,这样用户只需要对类型进行标记即可:`#[derive(HelloMacro)]`
以上两种方式并没有孰优孰劣,主要在于不同的类型是否可以使用同样的默认特征实现,如果可以,那过程宏的方式可以帮我们减少很多代码实现:
以上两种方式并没有孰优孰劣,主要在于不同的类型是否可以使用同样的默认特征实现,如果可以,那过程宏的方式可以帮我们减少很多代码实现:
```rust
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
@ -203,6 +224,7 @@ fn main() {
```
简单吗?简单!不过为了实现这段代码展示的功能,我们还需要创建相应的过程宏才行。 首先,创建一个新的工程用于演示:
```shell
$ cargo new hello_macro
$ cd hello_macro/
@ -212,6 +234,7 @@ $ touch src/lib.rs
此时,`src` 目录下包含两个文件 `lib.rs``main.rs`,前者是 `lib` 包根,后者是二进制包根,如果大家对包根不熟悉,可以看看[这里](https://course.rs/basic/crate-module/crate.html)。
接下来,先在 `src/lib.rs` 中定义过程宏所需的 `HelloMacro` 特征和其关联函数:
```rust
pub trait HelloMacro {
fn hello_macro();
@ -219,6 +242,7 @@ pub trait HelloMacro {
```
然后在 `src/main.rs` 中编写主体代码,首先映入大家脑海的可能会是如下实现:
```rust
use hello_macro::HelloMacro;
@ -246,6 +270,7 @@ fn main() {
但是这种方式有个问题如果想要实现不同的招呼内容就需要为每一个类型都实现一次相应的特征Rust 不支持反射,因此我们无法在运行时获得类型名。
使用宏,就不存在这个问题:
```rust
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
@ -265,11 +290,13 @@ fn main() {
简单明了的代码总是令人愉快,为了让代码运行起来,还需要定义下过程宏。就如前文提到的,目前只能在单独的包中定义过程宏,尽管未来这种限制会被取消,但是现在我们还得遵循这个规则。
宏所在的包名自然也有要求,必须以 `derive` 为后缀,对于 `hello_macro` 宏而言,包名就应该是 `hello_macro_derive`。在之前创建的 `hello_macro` 项目根目录下,运行如下命令,创建一个单独的 `lib` 包:
```rust
cargo new hello_macro_derive --lib
```
至此, `hello_macro` 项目的目录结构如下:
```shell
hello_macro
├── Cargo.toml
@ -285,6 +312,7 @@ hello_macro
由于过程宏所在的包跟我们的项目紧密相连,因此将它放在项目之中。现在,问题又来了,该如何在项目的 `src/main.rs` 中引用 `hello_macro_derive` 包的内容?
方法有两种,第一种是将 `hello_macro_derive` 发布到 `crates.io``github` 中,就像我们引用的其它依赖一样;另一种就是使用相对路径引入的本地化方式,修改 `hello_macro/Cargo.toml` 文件添加以下内容:
```toml
[dependencies]
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }
@ -297,7 +325,9 @@ hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }
接下来,就到了重头戏环节,一起来看看该如何定义过程宏。
#### 定义过程宏
首先,在 `hello_macro_derive/Cargo.toml` 文件中添加以下内容:
```toml
[lib]
proc-macro = true
@ -310,6 +340,7 @@ quote = "1.0"
其中 `syn``quote` 依赖包都是定义过程宏所必需的,同时,还需要在 `[lib]` 中将过程宏的开关开启 : `proc-macro = true`
其次,在 `hello_macro_derive/src/lib.rs` 中添加如下代码:
```rust
extern crate proc_macro;
@ -333,12 +364,12 @@ pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
`proc_macro` 包是 Rust 自带的,因此无需在 `Cargo.toml` 中引入依赖,它包含了相关的编译器 `API`,可以用于读取和操作 Rust 源代码。
由于我们为 `hello_macro_derive` 函数标记了 `#[proc_macro_derive(HelloMacro)]`,当用户使用 `#[derive(HelloMacro)]` 标记了他的类型后,`hello_macro_derive` 函数就将被调用。这里的秘诀就是特征名 `HelloMacro`,它就像一座桥梁,将用户的类型和过程宏联系在一起。
由于我们为 `hello_macro_derive` 函数标记了 `#[proc_macro_derive(HelloMacro)]`,当用户使用 `#[derive(HelloMacro)]` 标记了他的类型后,`hello_macro_derive` 函数就将被调用。这里的秘诀就是特征名 `HelloMacro`,它就像一座桥梁,将用户的类型和过程宏联系在一起。
`syn` 将字符串形式的 Rust 代码解析为一个 AST 树的数据结构,该数据结构可以在随后的 `impl_hello_macro` 函数中进行操作。最后,操作的结果又会被 `quote` 包转换回 Rust 代码。这些包非常关键,可以帮我们节省大量的精力,否则你需要自己去编写支持代码解析和还原的解析器,这可不是一件简单的任务!
`syn::parse` 调用会返回一个 `DeriveInput` 结构体来代表解析后的 Rust 代码:
```rust
DeriveInput {
// --snip--
@ -369,6 +400,7 @@ DeriveInput {
大家可能会注意到在 `hello_macro_derive` 函数中有 `unwrap` 的调用,也许会以为这是为了演示目的,没有做错误处理,实际上并不是的。由于该函数只能返回 `TokenStream` 而不是 `Result`,那么在报错时直接 `panic` 来抛出错误就成了相当好的选择。当然,这里实际上还是做了简化,在生产项目中,你应该通过 `panic!``expect` 抛出更具体的报错信息。
至此,这个函数大家应该已经基本理解了,下面来看看如何构建特征实现的代码,也是过程宏的核心目标:
```rust
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
@ -397,6 +429,7 @@ fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
- 可以减少一次 `String` 带来的内存分配
至此,过程宏的定义、特征定义、主体代码都已经完成,运行下试试:
```shell
$ cargo run
Running `target/debug/hello_macro`
@ -409,15 +442,18 @@ Bingo虽然过程有些复杂但是结果还是很喜人我们终于完
接下来,再来看看过程宏的另外两种类型跟 `derive` 类型有何区别。
## 类属性宏(Attribute-like macros)
类属性过程宏跟 `derive` 宏类似,但是前者允许我们定义自己的属性。除此之外,`derive` 只能用于结构体和枚举,而类属性宏可以用于其它类型项,例如函数。
假设我们在开发一个 `web` 框架,当用户通过 `HTTP GET` 请求访问 `/` 根路径时,使用 `index` 函数为其提供服务:
```rust
#[route(GET, "/")]
fn index() {
```
如上所示,代码功能非常清晰、简洁,这里的 `#[route]` 属性就是一个过程宏,它的定义函数大概如下:
```rust
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
@ -431,31 +467,34 @@ pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
除此之外,类属性宏跟 `derive` 宏的工作方式并无区别:创建一个包,类型是 `proc-macro`,接着实现一个函数用于生成想要的代码。
## 类函数宏(Function-like macros)
类函数宏可以让我们定义像函数那样调用的宏,从这个角度来看,它跟声明宏 `macro_rules` 较为类似。
区别在于,`macro_rules` 的定义形式与 `match` 匹配非常相像,而类函数宏的定义形式则类似于之前讲过的两种过程宏:
```rust
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
```
而使用形式则类似于函数调用:
```rust
let sql = sql!(SELECT * FROM posts WHERE id=1);
```
大家可能会好奇,为何我们不使用声明宏 `macro_rules` 来定义呢?原因是这里需要对 `SQL` 语句进行解析并检查其正确性,这个复杂的过程是 `macro_rules` 难以对付的,**而过程宏相比起来就会灵活的多**。
## 补充学习资料
1. [dtolnay/proc-macro-workshop](https://github.com/dtolnay/proc-macro-workshop),学习如何编写过程宏
2. [The Little Book of Rust Macros](https://veykril.github.io/tlborm/),学习如何编写声明宏 `macro_rules!`
3. [syn](https://crates.io/crates/syn) 和 [quote](https://crates.io/crates/quote) ,用于编写过程宏的包,它们的文档有很多值得学习的东西
4. [Structuring, testing and debugging procedural macro crates](https://www.reddit.com/r/rust/comments/rjumsg/any_good_resources_for_learning_rust_macros/)从测试、debug、结构化的角度来编写过程宏
5. [blog.turbo.fish](https://blog.turbo.fish),里面的过程宏系列文章值得一读
## 总结
Rust 中的宏主要分为两大类:声明宏和过程宏。
声明宏目前使用 `macro_rules` 进行创建,它的形式类似于 `match` 匹配,对于用户而言,可读性和维护性都较差。由于其存在的问题和限制,在未来, `macro_rules` 会被 `deprecated`Rust 会使用一个新的声明宏来替代它。

@ -1,5 +1,3 @@
# SIMD
由于 Rust 最新也是万众瞩目的 `Portable SIMD` 还没有完全成熟,只能在 `nightly` 版本中使用,因此功能上可能还存在变数,鉴于此,本文不会深入介绍在 Rust 中如何编写 SIMD 代码,而是将目光聚集在两个点上:何为 `SIMD` 以及关于 `Portable SIMD` 的简单介绍。

@ -1,9 +1,11 @@
# `Box<T>` 堆对象分配
关于作者帅不帅,估计争议还挺多的,但是如果说 `Box<T>` 是不是Rust中最常见的智能指针那估计没有任何争议。因为 `Box<T>` 允许你将一个值分配到堆上,然后在栈上保留一个智能指针指向堆上的数据。
关于作者帅不帅,估计争议还挺多的,但是如果说 `Box<T>` 是不是 Rust 中最常见的智能指针,那估计没有任何争议。因为 `Box<T>` 允许你将一个值分配到堆上,然后在栈上保留一个智能指针指向堆上的数据。
之前我们在[所有权章节](https://course.rs/basic/ownership/ownership.html#栈stack与堆heap)简单讲过堆栈的概念,这里再补充一些。
## Rust 中的堆栈
高级语言 Python/Java 等往往会弱化堆栈的概念,但是要用好 C/C++/Rust就必须对堆栈有深入的了解原因是两者的内存管理方式不同前者有 GC 垃圾回收机制,因此无需你去关心内存的细节。
栈内存从高位地址向下增长,且栈内存是连续分配的,一般来说**操作系统对栈内存的大小都有限制**,因此 C 语言中无法创建任意长度的数组。在 Rust 中,`main` 线程的[栈大小是 `8MB`](https://course.rs/pitfalls/stack-overflow.html),普通线程是 `2MB`,在函数调用时会在其中创建一个临时栈空间,调用结束后 Rust 会让这个栈空间里的对象自动进入 `Drop` 流程,最后栈顶指针自动移动到上一个调用栈顶,无需程序员手动干预,因而栈内存申请和释放是非常高效的。
@ -11,6 +13,7 @@
与栈相反,堆上内存则是从低位地址向上增长,**堆内存通常只受物理内存限制**,而且通常是不连续的,因此从性能的角度看,栈往往比堆更高。
相比其它语言Rust 堆上对象还有一个特殊之处,它们都拥有一个所有者,因此受所有权规则的限制:当赋值时,发生的是所有权的转移(只需浅拷贝栈上的引用或智能指针即可),例如以下代码:
```rust
fn main() {
let b = foo("world");
@ -26,6 +29,7 @@ fn foo(x: &str) -> String {
`foo` 函数中,`a` 是 `String` 类型,它其实是一个智能指针结构体,该智能指针存储在函数栈中,指向堆上的字符串数据。当被从 `foo` 函数转移给 `main` 中的 `b` 变量时,栈上的智能指针被复制一份赋予给 `b`,而底层数据无需发生改变,这样就完成了所有权从 `foo` 函数内部到 `b` 的转移。
#### 堆栈的性能
很多人可能会觉得栈的性能肯定比堆高,其实未必。 由于我们在后面的性能专题会专门讲解堆栈的性能问题,因此这里就大概给出结论:
- 小型数据,在栈上的分配性能和读取性能都要比堆上高
@ -35,6 +39,7 @@ fn foo(x: &str) -> String {
总之,栈的分配速度肯定比堆上快,但是读取速度往往取决于你的数据能不能放入寄存器或 CPU 高速缓存。 因此不要仅仅因为堆上性能不如栈这个印象,就总是优先选择栈,导致代码更复杂的实现。
## Box 的使用场景
由于 `Box` 是简单的封装,除了将值存储在堆上外,并没有其它性能上的损耗。而性能和功能往往是鱼和熊掌,因此 `Box` 相比其它智能指针,功能较为单一,可以在以下场景中使用它:
- 特意的将数据分配在堆上
@ -45,7 +50,9 @@ fn foo(x: &str) -> String {
以上场景,我们在本章将一一讲解,后面车速较快,请系好安全带。
#### 使用 `Box<T>` 将数据存储在堆上
如果一个变量拥有一个数值 `let a = 3`,那变量 `a` 必然是存储在栈上的,那如果我们想要 `a` 的值存储在堆上就需要使用 `Box<T>`
```rust
fn main() {
let a = Box::new(3);
@ -65,9 +72,11 @@ fn main() {
以上的例子在实际代码中其实很少会存在因为将一个简单的值分配到堆上并没有太大的意义。将其分配在栈上由于寄存器、CPU 缓存的原因,它的性能将更好,而且代码可读性也更好。
#### 避免栈上数据的拷贝
当栈上数据转移所有权时,实际上是把数据拷贝了一份,最终新旧变量各自拥有不同的数据,因此所有权并未转移。
而堆上则不然,底层数据并不会被拷贝,转移所有权仅仅是复制一份栈中的指针,再将新的指针赋予新的变量,然后让拥有旧指针的变量失效,最终完成了所有权的转移:
```rust
fn main() {
// 在栈上创建一个长度为1000的数组
@ -93,9 +102,11 @@ fn main() {
从以上代码,可以清晰看出大块的数据为何应该放入堆中,此时 `Box` 就成为了我们最好的帮手。
#### 将动态大小类型变为 Sized 固定大小类型
Rust 需要在编译时知道类型占用多少空间,如果一种类型在编译时无法知道具体的大小,那么被称为动态大小类型 DST。
其中一种无法在编译时知道大小的类型是**递归类型**:在类型定义中又使用到了自身,或者说该类型的值的一部分可以是相同类型的其它值,这种值的嵌套理论上可以无限进行下去,所以 Rust 不知道递归类型需要多少空间:
```rust
enum List {
Cons(i32, List),
@ -104,6 +115,7 @@ enum List {
```
以上就是函数式语言中常见的 `Cons List`,它的每个节点包含一个 `i32` 值,还包含了一个新的 `List`因此这种嵌套可以无限进行下去Rust 认为该类型是一个 DST 类型,并给予报错:
```console
error[E0072]: recursive type `List` has infinite size //递归类型 `List` 拥有无限长的大小
--> src/main.rs:3:1
@ -115,6 +127,7 @@ error[E0072]: recursive type `List` has infinite size //递归类型 `List` 拥
```
此时若想解决这个问题,就可以使用我们的 `Box<T>`
```rust
enum List {
Cons(i32, Box<List>),
@ -125,6 +138,7 @@ enum List {
只需要将 `List` 存储到堆上,然后使用一个智能指针指向它,即可完成从 DST 到 Sized 类型(固定大小类型)的华丽转变。
#### 特征对象
在 Rust 中,想实现不同类型组成的数组只有两个办法:枚举和特征对象,前者限制较多,因此后者往往是最常用的解决办法。
```rust
@ -165,7 +179,9 @@ fn main() {
其实,特征也是 DST 类型,而特征对象在做的就是将 DST 类型转换为固定大小类型。
## Box 内存布局
先来看看 `Vec<i32>` 的内存布局:
```rust
(stack) (heap)
┌──────┐ ┌───┐
@ -182,6 +198,7 @@ fn main() {
之前提到过 `Vec``String` 都是智能指针,从上图可以看出,该智能指针存储在栈中,然后指向堆上的数组数据。
那如果数组中每个元素都是一个 `Box` 对象呢?来看看 `Vec<Box<i32>>` 的内存布局:
```rust
(heap)
(stack) (heap) ┌───┐
@ -203,6 +220,7 @@ fn main() {
可以看出智能指针 `vec2` 依然是存储在栈上,然后指针指向一个堆上的数组,该数组中每个元素都是一个 `Box` 智能指针,最终 `Box` 智能指针又指向了存储在堆上的实际值。
因此当我们从数组中取出某个元素时,取到的是对应的智能指针 `Box`,需要对该智能指针进行解引用,才能取出最终的值:
```rust
fn main() {
let arr = vec![Box::new(1), Box::new(2)];
@ -216,11 +234,12 @@ fn main() {
- 使用 `&` 借用数组中的元素,否则会报所有权错误
- 表达式不能隐式的解引用,因此必须使用 `**` 做两次解引用,第一次将 `&Box<i32>` 类型转成 `Box<i32>`,第二次将 `Box<i32>` 转成 `i32`
## Box::leak
`Box` 中还提供了一个非常有用的关联函数:`Box::leak`,它可以消费掉 `Box` 并且强制目标值从内存中泄漏,读者可能会觉得,这有啥用啊?
其实还真有点用,例如,你可以把一个 `String` 类型,变成一个 `'static` 生命周期的 `&str` 类型:
```rust
fn main() {
let s = gen_static_str();
@ -242,11 +261,13 @@ fn gen_static_str() -> &'static str{
又有读者要问了,我还可以手动为变量标注 `'static` 啊。其实你标注的 `'static` 只是用来忽悠编译器的,但是超出作用域,一样被释放回收。而使用 `Box::leak` 就可以将一个运行期的值转为 `'static`
#### 使用场景
光看上面的描述,大家可能还是云里雾里、一头雾水。
那么我说一个简单的场景,**你需要一个在运行期初始化的值,但是可以全局有效,也就是和整个程序活得一样久**,那么久可以使用 `Box::leak`,例如有一个存储配置的结构体实例,它是在运行期动态插入内容,那么就可以将其转为全局有效,虽然 `Rc/Arc` 也可以实现此功能,但是 `Box::leak` 是性能最高的。
## 总结
`Box` 背后是调用 `jemalloc` 来做内存管理,所以堆上的空间无需我们的手动管理。与此类似,带 GC 的语言中的对象也是借助于 `Box` 概念来实现的,**一切皆对象 = 一切皆 Box** 只不过我们无需自己去 `Box` 罢了。
其实很多时候编译器的鞭笞可以助我们更快的成长例如所有权规则里的借用、move、生命周期就是编译器在教我们做人哦不是是教我们深刻理解堆栈、内存布局、作用域等等你在其它 GC 语言无需去关注的东西。刚开始是很痛苦,但是一旦熟悉了这套规则,写代码的效率和代码本身的质量将飞速上升,直到你可以用 Java 开发的效率写出 Java 代码不可企及的性能和安全性,最终 Rust 语言所谓的开发效率低、心智负担高,对你来说终究不是个事。

@ -1,4 +1,5 @@
# Cell 和 RefCell
Rust 的编译器之严格可以说是举世无双。特别是在所有权方面Rust 通过严格的规则来保证所有权和借用的正确性,最终为程序的安全保驾护航。
但是严格是一把双刃剑,带来安全提升的同时,损失了灵活性,有时甚至会让用户痛苦不堪、怨声载道。因此 Rust 提供了 `Cell``RefCell` 用于内部可变性,简而言之,可以在拥有不可变引用的同时修改目标数据,对于正常的代码实现来说,这个是不可能做到的(要么一个可变借用,要么多个不可变借用)。
@ -6,7 +7,9 @@ Rust 的编译器之严格,可以说是举世无双。特别是在所有权方
> 内部可变性的实现是因为 Rust 使用了 `unsafe` 来做到这一点,但是对于使用者来说,这些都是透明的,因为这些不安全代码都被封装到了安全的 API 中
## Cell
`Cell``RefCell` 在功能上没有区别,区别在于 `Cell<T>` 适用于 `T` 实现 `Copy` 的情况:
```rust
use std::cell::Cell;
fn main() {
@ -24,11 +27,13 @@ fn main() {
- `c.get` 用来取值,`c.set` 用来设置新值
取到值保存在 `one` 变量后,还能同时进行修改,这个违背了 Rust 的借用规则,但是由于 `Cell` 的存在,我们很优雅地做到了这一点,但是如果你尝试在 `Cell` 中存放`String`
```rust
let c = Cell::new(String::from("asdf"));
```
```
编译器会立刻报错,因为 `String` 没有实现 `Copy` 特征:
```console
| pub struct String {
| ----------------- doesn't satisfy `String: Copy`
@ -38,17 +43,19 @@ fn main() {
```
## RefCell
由于 `Cell` 类型针对的是实现了 `Copy` 特征的值类型,因此在实际开发中,`Cell` 使用的并不多,因为我们要解决的往往是可变、不可变引用共存导致的问题,此时就需要借助于 `RefCell` 来达成目的。
我们可以将所有权、借用规则与这些智能指针做一个对比:
| Rust规则 | 智能指针带来的额外规则 |
|--------|-------------|
| 一个数据只有一个所有者| `Rc/Arc`让一个数据可以拥有多个所有者 |
| Rust 规则 | 智能指针带来的额外规则 |
| ------------------------------------ | --------------------------------------- |
| 一个数据只有一个所有者 | `Rc/Arc`让一个数据可以拥有多个所有者 |
| 要么多个不可变借用,要么一个可变借用 | `RefCell`实现编译期可变、不可变引用共存 |
| 违背规则导致**编译错误** | 违背规则导致**运行时`panic`** |
| 违背规则导致**编译错误** | 违背规则导致**运行时`panic`** |
可以看出,`Rc/Arc` 和 `RefCell` 合在一起,解决了 Rust 中严苛的所有权和借用规则带来的某些场景下难使用的问题。但是它们并不是银弹,例如 `RefCell` 实际上并没有解决可变引用和引用可以共存的问题,只是将报错从编译期推迟到运行时,从编译器错误变成了 `panic` 异常:
```rust
use std::cell::RefCell;
@ -62,6 +69,7 @@ fn main() {
```
上面代码在编译期不会报任何错误,你可以顺利运行程序:
```console
thread 'main' panicked at 'already borrowed: BorrowMutError', src/main.rs:6:16
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
@ -70,6 +78,7 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
但是依然会因为违背了借用规则导致了运行期 `panic`,这非常像中国的天网,它也许会被罪犯蒙蔽一时,但是并不会被蒙蔽一世,任何导致安全风险的存在都将不能被容忍,法网恢恢,疏而不漏。
#### RefCell 为何存在
相信肯定有读者有疑问了,这么做有任何意义吗?还不如在编译期报错,至少能提前发现问题,而且性能还更好。
存在即合理,究其根因,在于 Rust 编译期的**宁可错杀,绝不放过**的原则,当编译器不能确定你的代码是否正确时,就统统会判定为错误,因此难免会导致一些误报。
@ -80,7 +89,6 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
总之,当你确信编译器误报但不知道该如何解决时,或者你有一个引用类型,需要被四处使用和修改然后导致借用关系难以管理时,都可以优先考虑使用 `RefCell`
#### RefCell 简单总结
- 与 `Cell` 用于可 `Copy` 的值不同,`RefCell` 用于引用
@ -89,13 +97,16 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
- 使用 `RefCell` 时,违背借用规则会导致运行期的 `panic`
## 选择 `Cell` 还是 `RefCell`
根据本文的内容,我们可以大概总结下两者的区别:
- `Cell` 只适用于 `Copy` 类型,用于提供值,而 `RefCell` 用于提供引用
- `Cell` 不会 `panic`,而 `RefCell`
#### 性能比较
`Cell` 没有额外的性能损耗,例如以下两段代码的性能其实是一致的:
```rust
// code snipet 1
let x = Cell::new(1);
@ -120,12 +131,12 @@ println!("{}", x);
`Cell``zero cost` 不同,`RefCell` 其实是有一点运行期开销的,原因是它包含了一个字大小的“借用状态”指示器,该指示器在每次运行时借用时都会被修改,进而产生一点开销。
总之,当非要使用内部可变性时,首选 `Cell`,只有你的类型没有实现 `Copy` 时,才去选择 `RefCell`
## 内部可变性
之前我们提到 `RefCell` 具有内部可变性,何为内部可变性?简单来说,对一个不可变的值进行可变借用,但这个并不符合 Rust 的基本借用规则:
```rust
fn main() {
let x = 5;
@ -136,6 +147,7 @@ fn main() {
上面的代码会报错,因为我们不能对一个不可变的值进行可变借用,这会破坏 Rust 的安全性保证,相反,你可以对一个可变值进行不可变借用。原因是:当值不可变时,可能会有多个不可变的引用指向它,此时若将修改其中一个为可变的,会造成可变引用与不可变引用共存的情况;而当值可变时,最多只会有一个可变引用指向它,将其修改为不可变,那么最终依然是只有一个不可变的引用指向它。
虽然基本借用规则是 Rust 的基石,然而在某些场景中,一个值可以在其方法内部被修改,同时对于其它代码不可变,是很有用的:
```rust
// 定义在外部库中的特征
pub trait Messenger {
@ -145,7 +157,7 @@ pub trait Messenger {
// --------------------------
// 我们的代码中的数据结构和实现
struct MsgQueue {
msg_cache: Vec<String>,
msg_cache: Vec<String>,
}
impl Messenger for MsgQueue {
@ -158,6 +170,7 @@ impl Messenger for MsgQueue {
如上所示,外部库中定义了一个消息发送器特征 `Messenger`,它只有一个发送消息的功能:`fn send(&self, msg: String)`,因为发送消息不需要修改自身,因此原作者在定义时,使用了 `&self` 的不可变借用,这个无可厚非。
我们要在自己的代码中使用该特征实现一个异步消息队列,出于性能的考虑,消息先写到本地缓存(内存)中,然后批量发送出去,因此在 `send` 方法中,需要将消息先行插入到本地缓存 `msg_cache` 中。但是问题来了,该 `send` 方法的签名是 `&self`,因此上述代码会报错:
```console
error[E0596]: cannot borrow `self.msg_cache` as mutable, as it is behind a `&` reference
--> src/main.rs:11:9
@ -170,6 +183,7 @@ error[E0596]: cannot borrow `self.msg_cache` as mutable, as it is behind a `&` r
```
在报错的同时,编译器大聪明还善意地给出了提示:将 `&self` 修改为 `&mut self`,但是。。。我们实现的特征是定义在外部库中,因此该签名根本不能修改。值此危急关头, `RefCell` 闪亮登场:
```rust
use std::cell::RefCell;
pub trait Messenger {
@ -196,8 +210,10 @@ fn main() {
这个 MQ 功能很弱,但是并不妨碍我们演示内部可变性的核心用法:通过包裹一层 `RefCell`,成功的让 `&self` 中的 `msg_cache` 成为一个可变值,然后实现对其的修改。
## Rc + RefCell 组合使用
## Rc + RefCell 组合使用
在 Rust 中,一个常见的组合就是 `Rc``RefCell` 在一起使用,前者可以实现一个数据拥有多个所有者,后者可以实现数据的可变性:
```rust
use std::cell::RefCell;
use std::rc::Rc;
@ -219,20 +235,23 @@ fn main() {
由于 `Rc` 的所有者们共享同一个底层的数据,因此当一个所有者修改了数据时,会导致全部所有者持有的数据都发生了变化。
程序的运行结果也在预料之中:
```console
RefCell { value: "我很善变,还拥有多个主人, on yeah!" }
RefCell { value: "我很善变,还拥有多个主人, on yeah!" }
RefCell { value: "我很善变,还拥有多个主人, on yeah!" }
```
#### 性能损耗
相信这两者组合在一起使用时,很多人会好奇到底性能如何,下面我们来简单分析下。
首先给出一个大概的结论,这两者结合在一起使用的性能其实非常高,大致相当于没有线程安全版本的 C++ `std::shared_ptr` 指针事实上C++ 这个指针的主要开销也在于原子性这个并发原语上,毕竟线程安全在哪个语言中开销都不小。
#### 内存损耗
两者结合的数据结构与下面类似:
```rust
struct Wrapper<T> {
// Rc
@ -250,20 +269,21 @@ struct Wrapper<T> {
从上面可以看出,从对内存的影响来看,仅仅多分配了三个`usize/isize`,并没有其它额外的负担。
#### CPU 损耗
从CPU来看损耗如下
从 CPU 来看,损耗如下:
- 对 `Rc<T>` 解引用是免费的(编译期),但是 `*` 带来的间接取值并不免费
- 克隆 `Rc<T>` 需要将当前的引用计数跟 `0``usize::Max` 进行一次比较,然后将计数值加 1
- 释放drop `Rc<T>` 需要将计数值减 1 然后跟 `0` 进行一次比较
- 对 `RefCell` 进行不可变借用,需要将 `isize` 类型的借用计数加1然后跟 `0` 进行比较
- 对 `RefCell` 进行不可变借用,需要将 `isize` 类型的借用计数加 1然后跟 `0` 进行比较
- 对 `RefCell `的不可变借用进行释放,需要将 `isize` 减 1
- 对 `RefCell` 的可变借用大致流程跟上面差不多,但是需要先跟 `0` 比较,然后再减 1
- 对 `RefCell` 的可变借用进行释放,需要将 `isize` 加 1
其实这些细节不必过于关注,只要知道 CPU 消耗也非常低,甚至编译器还会对此进行进一步优化!
#### CPU 缓存 Miss
唯一需要担心的可能就是这种组合数据结构对于 CPU 缓存是否亲和,这个我们无法证明,只能提出来存在这个可能性,最终的性能影响还需要在实际场景中进行测试。
总之,分析这两者组合的性能还挺复杂的,大概总结下:
@ -273,12 +293,14 @@ struct Wrapper<T> {
- CPU 缓存可能也不够亲和
## 通过 `Cell::from_mut` 解决借用冲突
在 Rust 1.37 版本中新增了两个非常实用的方法:
- Cell::from_mut该方法将 `&mut T` 转为 `&Cell<T>`
- Cell::as_slice_of_cells该方法将 `&Cell<[T]>` 转为 `&[Cell<T>]`
这里我们不做深入的介绍,但是来看看如何使用这两个方法来解决一个常见的借用冲突问题:
```rust
fn is_even(i: i32) -> bool {
i % 2 == 0
@ -293,7 +315,9 @@ fn retain_even(nums: &mut Vec<i32>) {
nums.truncate(i);
}
```
以上代码会报错:
```console
error[E0502]: cannot borrow `*nums` as mutable because it is also borrowed as immutable
--> src/main.rs:8:9
@ -308,6 +332,7 @@ error[E0502]: cannot borrow `*nums` as mutable because it is also borrowed as im
```
很明显,报错是因为同时借用了不可变与可变引用,你可以通过索引的方式来避免这个问题:
```rust
fn retain_even(nums: &mut Vec<i32>) {
let mut i = 0;
@ -324,6 +349,7 @@ fn retain_even(nums: &mut Vec<i32>) {
但是这样就违背我们的初衷了,毕竟迭代器会让代码更加简洁,那么还有其它的办法吗?
这时就可以使用 `Cell` 新增的这两个方法:
```rust
use std::cell::Cell;
@ -345,8 +371,8 @@ fn retain_even(nums: &mut Vec<i32>) {
当然,以上代码的本质还是对 `Cell` 的运用,只不过这两个方法可以很方便的帮我们把 `&mut [T]` 类型转换成 `&[Cell<T>]` 类型。
## 总结
`Cell``RefCell` 都为我们带来了内部可变性这个重要特性,同时还将借用规则的检查从编译期推迟到运行期,但是这个检查并不能被绕过,该来早晚还是会来,`RefCell` 在运行期的报错会造成 `panic`
`RefCell` 适用于编译器误报或者一个引用被在多个代码中使用、修改以至于难于管理借用关系时,还有就是需要内部可变性时。
@ -354,4 +380,3 @@ fn retain_even(nums: &mut Vec<i32>) {
从性能上看,`RefCell` 由于是非线程安全的,因此无需保证原子性,性能虽然有一点损耗,但是依然非常好,而 `Cell` 则完全不存在任何额外的性能损耗。
`Rc``RefCell` 结合使用可以实现多个所有者共享同一份数据,非常好用,但是潜在的性能损耗也要考虑进去,建议对于热点代码使用时,做好 `benchmark`

@ -1,4 +1,5 @@
# Deref 解引用
何为智能指针?能不让你写出 ******s 形式的解引用,我认为就是智能: ),智能指针的名称来源,主要就在于它实现了 `Deref``Drop` 特征,这两个特征可以智能地帮助我们节省使用上的负担:
- `Deref` 可以让智能指针像引用那样工作,这样你就可以写出同时支持智能指针和引用的代码,例如 `*T`
@ -7,10 +8,11 @@
下面先来看看 `Deref` 特征是如何工作的。
## 通过 `*` 获取引用背后的值
在正式讲解 `Deref` 之前,我们先来看下常规引用的解引用。
在正式讲解 `Deref` 之前,我们先来看下常规引用的解引用。
常规引用是一个指针类型,包含了目标数据存储的内存地址。对常规引用使用 `*` 操作符,就可以通过解引用的方式获取到内存地址对应的数据值:
```rust
fn main() {
let x = 5;
@ -22,6 +24,7 @@ fn main() {
```
这里 `y` 就是一个常规引用,包含了值 `5` 所在的内存地址,然后通过解引用 `*y`,我们获取到了值 `5`。如果你试图执行 `assert_eq!(5, y);`,代码就会无情报错,因为你无法将一个引用与一个数值做比较:
```console
error[E0277]: can't compare `{integer}` with `&{integer}` //无法将{integer} 与&{integer}进行比较
--> src/main.rs:6:5
@ -29,14 +32,16 @@ error[E0277]: can't compare `{integer}` with `&{integer}` //无法将{integer}
6 | assert_eq!(5, y);
| ^^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
|
= help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
= help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
// 你需要为{integer}实现用于比较的特征PartialEq<&{integer}>
```
## 智能指针解引用
上面所说的解引用方式和其它大多数语言并无区别,但是 Rust 中将解引用提升到了一个新高度。考虑一下智能指针,它是一个结构体类型,如果你直接对它进行 `*myStruct`,显然编译器不知道该如何办,因此我们可以为智能指针结构体实现 `Deref` 特征。
实现 `Deref` 后的智能指针结构体,就可以像普通引用一样,通过 `*` 进行解引用,例如 `Box<T>` 智能指针:
```rust
fn main() {
let x = Box::new(1);
@ -47,6 +52,7 @@ fn main() {
智能指针 `x``*` 解引用为 `i32` 类型的值 `1`,然后再进行求和。
#### 定义自己的智能指针
现在,让我们一起来实现一个智能指针,功能上类似 `Box<T>`。由于 `Box<T>` 本身很简单,并没有包含类如长度、最大长度等信息,因此用一个元组结构体即可。
```rust
@ -60,6 +66,7 @@ impl<T> MyBox<T> {
```
`Box<T>` 一样,我们的智能指针也持有一个 `T` 类型的值,然后使用关联函数 `MyBox::new` 来创建智能指针。由于还未实现 `Deref` 特征,此时使用 `*` 肯定会报错:
```rust
fn main() {
let y = MyBox::new(5);
@ -69,6 +76,7 @@ fn main() {
```
运行后,报错如下:
```console
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
--> src/main.rs:12:19
@ -78,7 +86,9 @@ error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
```
##### 为智能指针实现 Deref 特征
现在来为 `MyBox` 实现 `Deref` 特征,以支持 `*` 解引用操作符:
```rust
use std::ops::Deref;
@ -99,7 +109,9 @@ impl<T> Deref for MyBox<T> {
之前报错的代码此时已能顺利编译通过。当然,标准库实现的智能指针要考虑很多边边角角情况,肯定比我们的实现要复杂。
## `*` 背后的原理
当我们对智能指针 `Box` 进行解引用时,实际上 Rust 为我们调用了以下方法:
```rust
*(y.deref())
```
@ -110,9 +122,10 @@ impl<T> Deref for MyBox<T> {
需要注意的是,`*` 不会无限递归替换,从 `*y``*(y.deref())` 只会发生一次,而不会继续进行替换然后产生形如 `*((y.deref()).deref())` 的怪物。
## 函数和方法中的隐式 Deref 转换
在函数和方法中Rust 提供了一个极其有用的隐式转换:`Deref `转换。简单来说,当一个实现了 `Deref` 特征的值被传给函数或方法时,会根据函数参数的要求,来决定使用该值原本的类型还是 `Deref` 后的类型,例如:
```rust
fn main() {
let s = String::from("hello world");
@ -131,7 +144,9 @@ fn display(s: &str) {
- 必须使用 `&s` 的方式来触发 `Deref`(仅引用类型的实参才会触发自动解引用)
#### 连续的隐式 Deref 转换
如果你以为 `Deref` 仅仅这点作用,那就大错特错了。`Deref` 可以支持连续的隐式转换,直到找到适合的形式为止:
```rust
fn main() {
let s = MyBox::new(String::from("hello world"));
@ -146,6 +161,7 @@ fn display(s: &str) {
这里我们使用了之前自定义的智能指针 `MyBox`,并将其通过连续的隐式转换变成 `&str` 类型:首先 `MyBox``Deref``String` 类型,结果并不能满足 `display` 函数参数的要求,编译器发现 `String` 还可以继续 `Deref``&str`,最终成功的匹配了函数参数。
想象一下,假如 `Rust` 没有提供这种隐式转换,我们该如何调用 `display` 函数?
```rust
fn main() {
let m = MyBox::new(String::from("Rust"));
@ -158,6 +174,7 @@ fn main() {
但是 `Deref` 并不是没有缺点,缺点就是:如果你不知道某个类型是否实现了 `Deref` 特征,那么在看到某段代码时,并不能在第一时间反应过来该代码发生了隐式的 `Deref` 转换。事实上,不仅仅是 `Deref`,在 Rust 中还有各种 `From/Into` 等等会给阅读代码带来一定负担的特征。还是那句话一切选择都是权衡有得必有失得了代码的简洁性往往就失去了可读性Go 语言就是一个刚好相反的例子。
再来看一下在方法、赋值中自动应用 `Deref` 的例子:
```rust
fn main() {
let s = MyBox::new(String::from("hello, world"));
@ -169,6 +186,7 @@ fn main() {
对于 `s1`,我们通过两次 `Deref``&str` 类型的值赋给了它(**赋值操作需要手动解引用**);而对于 `s2`,我们在其上直接调用方法 `to_string`,实际上 `MyBox` 根本没有没有实现该方法,能调用 `to_string`,完全是因为编译器对 `MyBox` 应用了 `Deref` 的结果(**方法调用会自动解引用**)。
## Deref 规则总结
在上面,我们零碎的介绍了不少关于 `Deref` 特征的知识,下面来通过较为正式的方式来对其规则进行下总结。
一个类型为 `T` 的对象 `foo`,如果 `T: Deref<Target=U>`,那么,相关 `foo` 的引用 `&foo` 在应用的时候会自动转换为 `&U`
@ -176,6 +194,7 @@ fn main() {
粗看这条规则,貌似有点类似于 `AsRef`,而跟 `解引用` 似乎风马牛不相及,实际里面有些玄妙之处。
#### 引用归一化
Rust 编译器实际上只能对 `&v` 形式的引用进行解引用操作,那么问题来了,如果是一个智能指针或者 `&&&&v` 类型的呢? 该如何对这两个进行解引用?
答案是Rust 会在解引用时自动把智能指针和 `&&&&v` 做引用归一化操作,转换成 `&v` 形式,最终再对 `&v` 进行解引用:
@ -183,8 +202,8 @@ Rust 编译器实际上只能对 `&v` 形式的引用进行解引用操作,那
- 把智能指针比如在库中定义的Box、Rc、Arc、Cow 等)从结构体脱壳为内部的引用类型,也就是转成结构体内部的 `&v`
- 把多重`&`,例如 `&&&&&&&v`,归一成 `&v`
关于第二种情况,这么干巴巴的说,也许大家会迷迷糊糊的,我们来看一段标准库源码:
```rust
impl<T: ?Sized> Deref for &T {
type Target = T;
@ -197,7 +216,8 @@ impl<T: ?Sized> Deref for &T {
在这段源码中,`&T` 被自动解引用为 `T`,也就是 `&T: Deref<Target=T>` 。 按照这个代码,`&&&&T` 会被自动解引用为 `&&&T`,然后再自动解引用为 `&&T`,以此类推, 直到最终变成 `&T`
PS: 以下是 `LLVM` 编译后的部分中间层代码:
PS: 以下是 `LLVM` 编译后的部分中间层代码:
```rust
// Rust 代码
let mut _2: &i32;
@ -209,6 +229,7 @@ bb0: {
```
#### 几个例子
```rust
fn foo(s: &str) {}
@ -219,7 +240,6 @@ bb0: {
foo(&owned);
```
```rust
use std::rc::Rc;
@ -250,12 +270,15 @@ bb0: {
```
## 三种 Deref 转换
在之前,我们讲的都是不可变的 `Deref` 转换,实际上 Rust 还支持将一个可变的引用转换成另一个可变的引用以及将一个可变引用转换成不可变的引用,规则如下:
- 当 `T: Deref<Target=U>`,可以将 `&T` 转换成 `&U`,也就是我们之前看到的例子
- 当 `T: DerefMut<Target=U>`,可以将 `&mut T` 转换成 `&mut U`
- 当 `T: Deref<Target=U>`,可以将 `&mut T` 转换成 `&U`
来看一个关于 `DerefMut` 的例子:
```rust
struct MyBox<T> {
v: T,
@ -305,8 +328,8 @@ fn display(s: &mut String) {
如果从 Rust 的所有权和借用规则的角度考虑,当你拥有一个可变的引用,那该引用肯定是对应数据的唯一借用,那么此时将可变引用变成不可变引用并不会破坏借用规则;但是如果你拥有一个不可变引用,那同时可能还存在其它几个不可变的引用,如果此时将其中一个不可变引用转换成可变引用,就变成了可变引用与不可变引用的共存,最终破坏了借用规则。
## 总结
`Deref` 可以说是 Rust 中最常见的隐式类型转换,而且它可以连续的实现如 `Box<String> -> String -> &str` 的隐式转换,只要链条上的类型实现了 `Deref` 特征。
我们也可以为自己的类型实现 `Deref` 特征,但是原则上来说,只应该为自定义的智能指针实现 `Deref`。例如,虽然你可以为自己的自定义数组类型实现 `Deref` 以避免 `myArr.0[0]` 的使用形式,但是 Rust 官方并不推荐这么做,特别是在你开发三方库时。
我们也可以为自己的类型实现 `Deref` 特征,但是原则上来说,只应该为自定义的智能指针实现 `Deref`。例如,虽然你可以为自己的自定义数组类型实现 `Deref` 以避免 `myArr.0[0]` 的使用形式,但是 Rust 官方并不推荐这么做,特别是在你开发三方库时。

@ -1,10 +1,13 @@
# Drop 释放资源
在 Rust 中,我们之所以可以一拳打跑 GC 的同时一脚踢翻手动资源回收,主要就归功于 `Drop` 特征,同时它也是智能指针的必备特征之一。
## 学习目标
如何自动和手动释放资源及执行指定的收尾工作
## Rust 中的资源回收
在一些无 GC 语言中,程序员在一个变量无需再被使用时,需要手动释放它占用的内存资源,如果忘记了,那么就会发生内存泄漏,最终臭名昭著的 `OOM` 问题可能就会发生。
而在 Rust 中你可以指定在一个变量超出作用域时执行一段特定的代码最终编译器将帮你自动插入这段收尾代码。这样就无需在每一个使用该变量的地方都写一段代码来进行收尾工作和资源释放。不禁让人感叹Rust 的大腿真粗,香!
@ -12,6 +15,7 @@
没错,指定这样一段收尾工作靠的就是咱这章的主角 - `Drop` 特征。
## 一个不那么简单的 Drop 例子
```rust
struct HasDrop1;
struct HasDrop2;
@ -59,6 +63,7 @@ fn main() {
- 结构体中每个字段都有自己的 `Drop`
来看看输出:
```console
Running!
Dropping Foo!
@ -70,13 +75,16 @@ Dropping HasDrop2!
嗯,结果符合预期,每个资源都成功的执行了收尾工作,虽然 `println!` 这种收尾工作毫无意义 =,=
#### Drop 的顺序
观察以上输出,我们可以得出以下关于 `Drop` 顺序的结论
- **变量级别,按照逆序的方式**`_x` 在 `_foo` 之前创建,因此 `_x``_foo` 之后被 `drop`
- **结构体内部,按照顺序的方式**,结构体 `_x` 中的字段按照定义中的顺序依次 `drop`
#### 没有实现 Drop 的结构体
实际上,就算你不为 `_x` 结构体实现 `Drop` 特征,它内部的两个字段依然会调用 `drop`,移除以下代码,并观察输出:
```rust
impl Drop for HasTwoDrops {
fn drop(&mut self) {
@ -86,14 +94,17 @@ impl Drop for HasTwoDrops {
```
原因在于Rust 自动为几乎所有类型都实现了 `Drop` 特征,因此就算你不手动为结构体实现 `Drop`,它依然会调用默认实现的 `drop` 函数,同时再调用每个字段的 `drop` 方法,最终打印出:
```cnosole
Dropping HasDrop1!
Dropping HasDrop2!
```
## 手动回收
当使用智能指针来管理锁的时候,你可能希望提前释放这个锁,然后让其它代码能及时获得锁,此时就需要提前去手动 `drop`
但是在之前我们提到一个悬念,`Drop::drop` 只是借用了目标值的可变引用,所以,就算你提前调用了 `drop`,后面的代码依然可以使用目标值,但是这就会访问一个并不存在的值,非常不安全,好在 Rust 会阻止你:
```rust
#[derive(Debug)]
struct Foo;
@ -112,6 +123,7 @@ fn main() {
```
报错如下:
```console
error[E0040]: explicit use of destructor method
--> src/main.rs:37:9
@ -125,13 +137,14 @@ error[E0040]: explicit use of destructor method
如上所示,编译器直接阻止了我们调用 `Drop` 特征的 `drop` 方法,原因是对于 Rust 而言,不允许显式地调用析构函数(这是一个用来清理实例的通用编程概念)。好在在报错的同时,编译器还给出了一个提示:使用 `drop` 函数。
针对编译器提示的 `drop` 函数,我们可以大胆推测下:它能够拿走目标值的所有权。现在来看看这个猜测正确与否,以下是 `std::mem::drop` 函数的签名:
```rust
pub fn drop<T>(_x: T)
```
如上所示,`drop` 函数确实拿走了目标值的所有权,来验证下:
```rust
fn main() {
let foo = Foo;
@ -145,8 +158,8 @@ Bingo完美拿走了所有权而且这种实现保证了后续的使用必
细心的同学可能已经注意到,这里直接调用了 `drop` 函数,并没有引入任何模块信息,原因是该函数在[`std::prelude`](../../appendix/prelude.md)里。
## Drop 使用场景
对于 Drop 而言,主要有两个功能:
- 回收内存资源
@ -156,9 +169,10 @@ Bingo完美拿走了所有权而且这种实现保证了后续的使用必
在绝大多数情况下,我们都无需手动去 `drop` 以回收内存资源,因为 Rust 会自动帮我们完成这些工作,它甚至会对复杂类型的每个字段都单独的调用 `drop` 进行回收!但是确实有极少数情况,需要你自己来回收资源的,例如文件描述符、网络 socket 等,当这些值超出作用域不再使用时,就需要进行关闭以释放相关的资源,在这些情况下,就需要使用者自己来解决 `Drop` 的问题。
## 互斥的 Copy 和 Drop
我们无法为一个类型同时实现 `Copy``Drop` 特征。因为实现了 `Copy` 的特征会被编译器隐式的复制,因此非常难以预测析构函数执行的时间和频率。因此这些实现了 `Copy` 的类型无法拥有析构函数。
```rust
#[derive(Copy)]
struct Foo;
@ -169,7 +183,9 @@ impl Drop for Foo {
}
}
```
以上代码报错如下:
```console
error[E0184]: the trait `Copy` may not be implemented for this type; the type has a destructor
--> src/main.rs:24:10
@ -178,8 +194,8 @@ error[E0184]: the trait `Copy` may not be implemented for this type; the type ha
| ^^^^ Copy not allowed on types with destructors
```
## 总结
`Drop` 可以用于许多方面,来使得资源清理及收尾工作变得方便和安全,甚至可以用其创建我们自己的内存分配器!通过 `Drop` 特征和 Rust 所有权系统你无需担心之后的代码清理Rust 会自动考虑这些问题。
我们也无需担心意外的清理掉仍在使用的值,这会造成编译器错误:所有权系统确保引用总是有效的,也会确保 `drop` 只会在值不再被使用时被调用一次。

@ -1,4 +1,5 @@
# 智能指针
在各个编程语言中,指针的概念几乎都是相同的:**指针是一个包含了内存地址的变量,该内存地址引用或者指向了另外的数据**。
在 Rust 中,最常见的指针类型是引用,引用通过 `&` 符号表示。不同于其它语言,引用在 Rust 中被赋予了更深层次的含义,那就是:借用其它变量的值。引用本身很简单,除了指向某个值外并没有其它的功能,也不会造成性能上的额外损耗,因此是 Rust 中使用最多的指针类型。
@ -11,7 +12,6 @@ Rust 标准库中定义的那些智能指针,虽重但强,可以提供比引
在之前的章节中,实际上我们已经见识过多种智能指针,例如动态字符串 `String` 和动态数组 `Vec`,它们的数据结构中不仅仅包含了指向底层数据的指针,还包含了当前长度、最大长度等信息,其中 `String` 智能指针还提供了一种担保信息:所有的数据都是合法的 `UTF-8` 格式。
智能指针往往是基于结构体实现,它与我们自定义的结构体最大的区别在于它实现了 `Deref``Drop` 特征:
- `Deref` 可以让智能指针像引用那样工作,这样你就可以写出同时支持智能指针和引用的代码,例如 `*T`

@ -1,4 +1,5 @@
# Rc 与 Arc
Rust 所有权机制要求一个值只能有一个所有者,在大多数情况下,都没有问题,但是考虑以下情况:
- 在图数据结构中,多个边可能会拥有同一个节点,该节点直到没有边指向它时,才应该被释放清理
@ -9,22 +10,25 @@ Rust 所有权机制要求一个值只能有一个所有者,在大多数情况
这种实现机制就是 `Rc``Arc`,前者适用于单线程,后者适用于多线程。由于二者大部分情况下都相同,因此本章将以 `Rc` 作为讲解主体,对于 `Arc` 的不同之处,另外进行单独讲解。
## Rc<T>
引用计数(reference counting),顾名思义,通过记录一个数据被引用的次数来确定该数据是否正在被使用。当引用次数归零时,就代表该数据不再被使用,因此可以被清理释放。
`Rc` 正是引用计数的英文缩写。当我们**希望在堆上分配一个对象供程序的多个部分使用且无法确定哪个部分最后一个结束时,就可以使用 `Rc` 成为数据值的所有者**,例如之前提到的多线程场景就非常适合。
下面是经典的所有权被转移导致报错的例子:
```rust
fn main() {
let s = String::from("hello, world");
// s在这里被转移给a
let a = Box::new(s);
// 报错!此处继续尝试将 s 转移给 b
let b = Box::new(s);
let b = Box::new(s);
}
```
使用 `Rc` 就可以轻易解决:
```rust
use std::rc::Rc;
fn main() {
@ -38,9 +42,10 @@ fn main() {
以上代码我们使用 `Rc::new` 创建了一个新的 `Rc<String>` 智能指针并赋给变量 `a`,该指针指向底层的字符串数据。
智能指针 `Rc<T>` 在创建时还会将引用计数加1此时获取引用计数的关联函数 `Rc::strong_count` 返回的值将是 `1`
智能指针 `Rc<T>` 在创建时,还会将引用计数加 1此时获取引用计数的关联函数 `Rc::strong_count` 返回的值将是 `1`
#### Rc::clone
接着,我们又使用 `Rc::clone` 克隆了一份智能指针 `Rc<String>`,并将该智能指针的引用计数增加到 `2`
由于 `a``b` 是同一个智能指针的两个副本,因此通过它们两个获取引用计数的结果都是 `2`
@ -50,7 +55,9 @@ fn main() {
实际上在 Rust 中,还有不少 `clone` 都是浅拷贝,例如[迭代器的克隆](https://course.rs/pitfalls/iterator-everywhere.html)。
#### 观察引用计数的变化
使用关联函数 `Rc::strong_count` 可以获取当前引用计数的值,我们来观察下引用计数如何随着变量声明、释放而变化:
```rust
use std::rc::Rc;
fn main() {
@ -68,18 +75,20 @@ fn main() {
有几点值得注意:
- 由于变量 `c` 在语句块内部声明当离开语句块时它会因为超出作用域而被释放所以引用计数会减少1事实上这个得益于 `Rc<T>` 实现了 `Drop` 特征
- 由于变量 `c` 在语句块内部声明,当离开语句块时它会因为超出作用域而被释放,所以引用计数会减少 1事实上这个得益于 `Rc<T>` 实现了 `Drop` 特征
- `a`、`b`、`c` 三个智能指针引用计数都是同样的,并且共享底层的数据,因此打印计数时用哪个都行
- 无法看到的是:当 `a`、`b` 超出作用域后,引用计数会变成 0最终智能指针和它指向的底层字符串都会被清理释放
#### 不可变引用
事实上,`Rc<T>` 是指向底层数据的不可变的引用,因此你无法通过它来修改数据,这也符合 Rust 的借用规则:要么存在多个不可变借用,要么只能存在一个可变借用。
但是实际开发中我们往往需要对数据进行修改,这时单独使用 `Rc<T>` 无法满足我们的需求,需要配合其它数据类型来一起使用,例如内部可变性的 `RefCell<T>` 类型以及互斥锁 `Mutex<T>`。事实上,在多线程编程中,`Arc` 跟 `Mutext` 锁的组合使用非常常见,它们既可以让我们在不同的线程中共享数据,又允许在各个线程中对其进行修改。
#### 一个综合例子
考虑一个场景,有很多小工具,每个工具都有自己的主人,但是存在多个工具属于同一个主人的情况,此时使用 `Rc<T>` 就非常适合:
```rust
use std::rc::Rc;
@ -135,10 +144,10 @@ fn main() {
- `Rc` 只能用于同一线程内部,想要用于线程之间的对象共享,你需要使用 `Arc`
- `Rc<T>` 是一个智能指针,实现了 `Deref` 特征,因此你无需先解开 `Rc` 指针,再使用里面的 `T`,而是可以直接使用 `T`,例如上例中的 `gadget1.owner.name`
## 多线程无力的 Rc<T>
来看看在多线程场景使用 `Rc<T>` 会如何:
```rust
use std::rc::Rc;
use std::thread;
@ -157,6 +166,7 @@ fn main() {
由于我们还没有学习多线程的章节,上面的例子就特地简化了相关的实现。首先通过 `thread::spawn` 创建一个线程,然后使用 `move` 关键字把克隆出的 `s` 的所有权转移到线程中。
能够实现这一点,完全得益于 `Rc` 带来的多所有权机制,但是以上代码会报错:
```console
error[E0277]: `Rc<String>` cannot be sent between threads safely
```
@ -168,14 +178,17 @@ error[E0277]: `Rc<String>` cannot be sent between threads safely
好在天无绝人之路,一起来看看 Rust 为我们提供的功能类似但是多线程安全的 `Arc`
## Arc
`Arc``Atomic Rc` 的缩写,顾名思义:原子化的 `Rc<T>` 智能指针。原子化是一种并发原语,我们在后续章节会进行深入讲解,这里你只要知道它能保证我们的数据能够安全的在线程间共享即可。
#### Arc 的性能损耗
你可能好奇,为何不直接使用 `Arc`,还要画蛇添足弄一个 `Rc`,还有 Rust 的基本数据类型、标准库数据类型为什么不自动实现原子化操作?这样就不存在线程不安全的问题了。
原因在于原子化或者其它锁虽然可以带来的线程安全,但是都会伴随着性能损耗,而且这种性能损耗还不小。因此 Rust 把这种选择权交给你,毕竟需要线程安全的代码其实占比并不高,大部分时候我们开发的程序都在一个线程内。
`Arc``Rc` 拥有完全一样的 API修改起来很简单
```rust
use std::sync::Arc;
use std::thread;
@ -193,8 +206,8 @@ fn main() {
对了,两者还有一点区别:`Arc` 和 `Rc` 并没有定义在同一个模块,前者通过 `use std::sync::Arc` 来引入,后者通过 `use std::rc::Rc`
## 总结
在 Rust 中,所有权机制保证了一个数据只会有一个所有者,但如果你想要在图数据结构、多线程等场景中共享数据,这种机制会成为极大的阻碍。好在 Rust 为我们提供了智能指针 `Rc``Arc`,使用它们就能实现多个所有者共享一个数据的功能。
`Rc``Arc` 的区别在于,后者是原子化实现的引用计数,因此是线程安全的,可以用于多线程中共享数据。

@ -1,12 +1,15 @@
# unsafe 简介
圣人论迹不论心,论心世上无圣人,对于编程语言而言,亦是如此。
虽然在本章之前,我们学到的代码都是在编译期就得到了 Rust 的安全保障,但是在其内心深处也隐藏了一些阴暗面,在这些阴暗面里,内存安全就存在一些变数了:当不娴熟的开发者接触到这些阴暗面,就可能写出不安全的代码,因此我们称这种代码为 `unsafe` 代码块。
## 为何会有 unsafe
几乎每个语言都有 `unsafe` 关键字,但 Rust 语言使用 `unsafe` 的原因可能与其它编程语言还有所不同。
#### 过强的编译器
说来尴尬,`unsafe` 的存在主要是因为 Rust 的静态检查太强了,但是强就算了,它还很保守,这就会导致当编译器在分析代码时,一些正确代码会因为编译器无法分析出它的所有正确性,结果将这段代码拒绝,导致编译错误。
这种保守的选择确实也没有错,毕竟安全就是要防微杜渐,但是对于使用者来说,就不是那么愉快的事了,特别是当配合 Rust 的所有权系统一起使用时,有个别问题是真的棘手和难以解决。
@ -15,8 +18,8 @@
好在,当遇到这些情况时,我们可以使用 `unsafe` 来解决。此时,你需要替代编译器的部分职责对 `unsafe` 代码的正确性负责,例如在正常代码中不可能遇到的空指针解引用问题在 `unsafe` 中就可能会遇到,我们需要自己来处理好这些类似的问题。
#### 特定任务的需要
至于 `unsafe` 存在的另一个原因就是:它必须要存在。原因是计算机底层的一些硬件就是不安全的,如果 Rust 只允许你做安全的操作,那一些任务就无法完成,换句话说,我们还怎么跟 C++ 干架?
Rust 的一个主要定位就是系统编程,众所周知,系统编程就是底层编程,往往需要直接跟操作系统打交道,甚至于去实现一个操作系统。而为了实现底层系统编程,`unsafe` 就是必不可少的。
@ -24,8 +27,10 @@ Rust 的一个主要定位就是系统编程,众所周知,系统编程就是
在了解了为何会有 `unsafe` 后,我们再来看看,除了这些必要性,`unsafe` 还能给我们带来哪些超能力。
## unsafe 的超能力
使用 `unsafe` 非常简单,只需要将对应的代码块标记下即可:
```rust
```rust
fn main() {
let mut num = 5;
@ -50,20 +55,23 @@ fn main() {
在本章中,我们将着重讲解原生指针和 FFI 的使用。
## unsafe 的安全保证
曾经在 `reddit` 上有一个讨论还挺热闹的,是关于 `unsafe` 的命名是否合适,总之公有公理,婆有婆理,但有一点是不可否认的:虽然名称自带不安全,但是 Rust 依然提供了强大的安全支撑。
首先,`unsafe` 并不能绕过 Rust 的借用检查,也不能关闭任何 Rust 的安全检查规则,例如当你在 `unsafe` 中使用**引用**时,该有的检查一样都不会少。
因此 `unsafe` 能给大家提供的也仅仅是之前的 5 种超能力在使用这5种能力时编译器才不会进行内存安全方面的检查最典型的就是使用**原生指针**(引用和原生指针有很大的区别)。
因此 `unsafe` 能给大家提供的也仅仅是之前的 5 种超能力,在使用这 5 种能力时,编译器才不会进行内存安全方面的检查,最典型的就是使用**原生指针**(引用和原生指针有很大的区别)。
## 谈虎色变?
在网上充斥着这样的言论:`千万不要使用 unsafe因为它不安全`,甚至有些库会以没有 `unsafe` 代码作为噱头来吸引用户。事实上大可不必如果按照这个标准Rust 的标准库也将不复存在!
Rust 中的 `unsafe` 其实没有那么可怕,虽然听上去很不安全,但是实际上 Rust 依然提供了很多机制来帮我们提升了安全性,因此不必像对待 Go 语言的 `unsafe` 那样去畏惧于使用Rust中的 `unsafe`
Rust 中的 `unsafe` 其实没有那么可怕,虽然听上去很不安全,但是实际上 Rust 依然提供了很多机制来帮我们提升了安全性,因此不必像对待 Go 语言的 `unsafe` 那样去畏惧于使用 Rust 中的 `unsafe`
大致使用原则总结如下:没必要用时,就不要用,当有必要用时,就大胆用,但是尽量控制好边界,让 `unsafe` 的范围尽可能小。
## 控制 unsafe 的使用边界
`unsafe` 不安全,但是该用的时候就要用,在一些时候,它能帮助我们大幅降低代码实现的成本。
而作为使用者,你的水平决定了 `unsafe` 到底有多不安全,因此你需要在 `unsafe` 中小心谨慎地去访问内存。

@ -1,7 +1,9 @@
# 五种兵器
古龙有一部小说,名为"七种兵器",其中每一种都精妙绝伦,令人闻风丧胆,而 `unsafe` 也有五种兵器,它们可以让你拥有其它代码无法实现的能力,同时它们也像七种兵器一样令人闻风丧胆,下面一起来看看庐山真面目。
## 解引用原生指针
原生指针(raw pointer) 又称裸指针,在功能上跟引用类似,同时它也需要显式地注明可变性。但是又和引用有所不同,原生指针长这样: `*const T``*mut T`,它们分别代表了不可变和可变。
大家在之前学过 `*` 操作符,知道它可以用于解引用,但是在原生指针 `*const T` 中,这里的 `*` 只是类型名称的一部分,并没有解引用的含义。
@ -16,7 +18,9 @@
总之,原生指针跟 C 指针是非常像的,使用它需要以牺牲安全性为前提,但我们获得了更好的性能,也可以跟其它语言或硬件打交道。
#### 基于引用创建原生指针
下面的代码**基于值的引用**同时创建了可变和不可变的原生指针:
```rust
let mut num = 5;
@ -27,6 +31,7 @@ let r2 = &mut num as *mut i32;
`as` 可以用于强制类型转换,在[之前章节](https://course.rs/basic/converse.html)中有讲解。在这里,我们将引用 `&num / &mut num` 强转为相应的原生指针 `*const i32 / *mut i32`
细心的同学可能会发现,在这段代码中并没有 `unsafe` 的身影,原因在于:**创建原生指针是安全的行为,而解引用原生指针才是不安全的行为** :
```rust
fn main() {
let mut num = 5;
@ -40,7 +45,9 @@ fn main() {
```
#### 基于内存地址创建原生指针
在上面例子中,我们基于现有的引用来创建原生指针,这种行为是很安全的。但是接下来的方式就不安全了:
```rust
let address = 0x012345usize;
let r = address as *const i32;
@ -51,6 +58,7 @@ let r = address as *const i32;
同时编译器也有可能会优化这段代码,会造成没有任何内存访问发生,甚至程序还可能发生段错误(segmentation fault)。**总之,你几乎没有好的理由像上面这样实现代码,虽然它是可行的**。
如果真的要使用内存地址,也是类似下面的用法,先取地址,再使用,而不是凭空捏造一个地址:
```rust
use std::{slice::from_raw_parts, str::from_utf8_unchecked};
@ -81,10 +89,11 @@ fn main() {
以上代码同时还演示了访问非法内存地址会发生什么,大家可以试着去反注释这段代码试试。
#### 使用 * 解引用
#### 使用 \* 解引用
```rust
let a = 1;
let b: *const i32 = &a as *const i32;
let b: *const i32 = &a as *const i32;
let c: *const i32 = &a;
unsafe {
println!("{}", *c);
@ -96,7 +105,9 @@ unsafe {
以上代码另一个值得注意的点就是:除了使用 `as` 来显式的转换,我们还使用了隐式的转换方式 `let c: *const i32 = &a;`。在实际使用中,我们建议使用 `as` 来转换,因为这种显式的方式更有助于提醒用户:你在使用的指针是原生指针,需要小心。
#### 基于智能指针创建原生指针
还有一种创建原生指针的方式,那就是基于智能指针来创建:
```rust
let a: Box<i32> = Box::new(10);
// 需要先解引用a
@ -106,11 +117,13 @@ let c: *const i32 = Box::into_raw(a);
```
#### 小结
像之前代码演示的那样使用原生指针可以让我们创建两个可变指针都指向同一个数据如果使用安全的Rust你是无法做到这一点的违背了借用规则编译器会对我们进行无情的阻止。因此原生指针可以绕过借用规则但是由此带来的数据竞争问题就需要大家自己来处理了总之需要小心
像之前代码演示的那样,使用原生指针可以让我们创建两个可变指针都指向同一个数据,如果使用安全的 Rust你是无法做到这一点的违背了借用规则编译器会对我们进行无情的阻止。因此原生指针可以绕过借用规则但是由此带来的数据竞争问题就需要大家自己来处理了总之需要小心
既然这么危险,为何还要使用原生指针?除了之前提到的性能等原因,还有一个重要用途就是跟 `C` 语言的代码进行交互( FFI ),在讲解 FFI 之前,先来看看如何调用 unsafe 函数或方法。
## 调用 unsafe 函数或方法
`unsafe` 函数从外表上来看跟普通函数并无区别,唯一的区别就是它需要使用 `unsafe fn` 来进行定义。这种定义方式是为了告诉调用者:当调用此函数时,你需要注意它的相关需求,因为 Rust 无法担保调用者在使用该函数时能满足它所需的一切需求。
强制调用者加上 `unsafe` 语句块,就可以让他清晰的认识到,正在调用一个不安全的函数,需要小心看看文档,看看函数有哪些特别的要求需要被满足。
@ -123,6 +136,7 @@ fn main() {
```
如果试图像上面这样调用,编译器就会报错:
```shell
error[E0133]: call to unsafe function is unsafe and requires unsafe function or block
--> src/main.rs:3:5
@ -132,6 +146,7 @@ error[E0133]: call to unsafe function is unsafe and requires unsafe function or
```
按照报错提示,加上 `unsafe` 语句块后,就能顺利执行了:
```rust
unsafe {
dangerous();
@ -143,9 +158,11 @@ unsafe {
还有,`unsafe` 无需俄罗斯套娃,在 `unsafe` 函数体中使用 `unsafe` 语句块是多余的行为。
## 用安全抽象包裹 unsafe 代码
一个函数包含了 `unsafe` 代码不代表我们需要将整个函数都定义为 `unsafe fn`。事实上,在标准库中有大量的安全函数,它们内部都包含了 `unsafe` 代码块,下面我们一起来看看一个很好用的标准库函数:`split_at_mut`。
大家可以想象一下这个场景:需要将一个数组分成两个切片,且每一个切片都要求是可变的。类似需求在安全 Rust 中是很难实现的,因为要对同一个数组做两个可变借用:
```rust
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
@ -168,6 +185,7 @@ fn main() {
```
上面代码一眼看过去就知道会报错,因为我们试图在自定义的 `split_at_mut` 函数中,可变借用 `slice` 两次:
```shell
error[E0499]: cannot borrow `*slice` as mutable more than once at a time
--> src/main.rs:6:30
@ -209,7 +227,6 @@ fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
- `slice::from_raw_parts_mut` 函数通过指针和长度来创建一个新的切片,简单来说,该切片的初始地址是 `ptr`,长度为 `mid`
- `ptr.add(mid)` 可以获取第二个切片的初始地址,由于切片中的元素是 `i32` 类型,每个元素都占用了 4 个字节的内存大小,因此我们不能简单的用 `ptr + mid` 来作为初始地址,而应该使用 `ptr + 4 * mid`,但是这种使用方式并不安全,因此 `.add` 方法是最佳选择
由于 `slice::from_raw_parts_mut` 使用原生指针作为参数,因此它是一个 `unsafe fn`,我们在使用它时,就必须用 `unsafe` 语句块进行包裹,类似的,`.add` 方法也是如此(还是那句话,不要将无关的代码包含在 `unsafe` 语句块中)。
部分同学可能会有疑问,那这段代码我们怎么保证 `unsafe` 中使用的原生指针 `ptr``ptr.add(mid)` 是合法的呢?秘诀就在于 `assert!(mid <= len);` ,通过这个断言,我们保证了原生指针一定指向了 `slice` 切片中的某个元素,而不是一个莫名其妙的内存地址。
@ -217,6 +234,7 @@ fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
再回到我们的主题:**虽然 split_at_mut 使用了 `unsafe`,但我们无需将其声明为 `unsafe fn`**,这种情况下就是使用安全的抽象包裹 `unsafe` 代码,这里的 `unsafe` 使用是非常安全的,因为我们从合法数据中创建了的合法指针。
与之对比,下面的代码就非常危险了:
```rust
use std::slice;
@ -236,6 +254,7 @@ zsh: segmentation fault
不出所料,运行后看到了一个段错误。
## FFI
`FFI`Foreign Function Interface可以用来与其它语言进行交互但是并不是所有语言都这么称呼例如 Java 称之为 `JNIJava Native Interface`
`FFI` 之所以存在是由于现实中很多代码库都是由不同语言编写的,如果我们需要使用某个库,但是它是由其它语言编写的,那么往往只有两个选择:
@ -247,11 +266,12 @@ zsh: segmentation fault
还有在将 C/C++ 的代码重构为 Rust 时,先将相关代码引入到 Rust 项目中,然后逐步重构,也是不错的(为什么用不错来形容?因为重构一个有一定规模的 C/C++ 项目远没有想象中美好,因此最好的选择还是对于新项目使用 Rust 实现,老项目。。就让它先运行着吧)。
当然,除了 `FFI` 还有一个办法可以解决跨语言调用的问题那就是将其作为一个独立的服务然后使用网络调用的方式去访问HTTPgRPC都可以。
当然,除了 `FFI` 还有一个办法可以解决跨语言调用的问题那就是将其作为一个独立的服务然后使用网络调用的方式去访问HTTPgRPC 都可以。
言归正传,之前我们提到 `unsafe` 的另一个重要目的就是对 `FFI` 提供支持,它的全称是 `Foreign Function Interface`,顾名思义,通过 `FFI` , 我们的 Rust 代码可以跟其它语言的外部代码进行交互。
下面的例子演示了如何调用 C 标准库中的 `abs` 函数:
```rust
extern "C" {
fn abs(input: i32) -> i32;
@ -267,12 +287,15 @@ fn main() {
C 语言的代码定义在了 `extern` 代码块中, 而 `extern` 必须使用 `unsafe` 才能进行进行调用,原因在于其它语言的代码并不会强制执行 Rust 的规则,因此 Rust 无法对这些代码进行检查,最终还是要靠开发者自己来保证代码的正确性和程序的安全性。
#### ABI
`exetrn "C"` 代码块中,我们列出了想要调用的外部函数的签名。其中 `"C"` 定义了外部函数所使用的**应用二进制接口**`ABI` (Application Binary Interface)`ABI` 定义了如何在汇编层面来调用该函数。在所有 `ABI`C 语言的是最常见的。
#### 在其它语言中调用 Rust 函数
在 Rust 中调用其它语言的函数是让 Rust 利用其他语言的生态,那反过来可以吗?其他语言可以利用 Rust 的生态不?答案是肯定的。
我们可以使用 `extern` 来创建一个接口,其它语言可以通过该接口来调用相关的 Rust 函数。但是此处的语法与之前有所不同,之前用的是语句块,而这里是在函数定义时加上 `extern` 关键字,当然,别忘了指定相应的 `ABI`
```rust
#[no_mangle]
pub extern "C" fn call_from_c() {
@ -286,14 +309,16 @@ pub extern "C" fn call_from_c() {
因此,为了让 Rust 函数能顺利被其它语言调用,我们必须要禁止掉该功能。
## 访问或修改一个可变的静态变量
这部分我们在之前的[全局变量章节](https://course.rs/advance/global-variable.html#静态变量)中有过详细介绍,这里就不再赘述,大家可以前往此章节阅读。
## 实现 unsafe 特征
说实话,`unsafe` 的特征确实不多见,如果大家还记得的话,我们在之前的 [Send 和 Sync](https://course.rs/advance/concurrency-with-threads/send-sync.html#为原生指针实现sync) 章节中实现过 `unsafe` 特征 `Send`
之所以会有 `unsafe` 的特征,是因为该特征至少有一个方法包含有编译器无法验证的内容。`unsafe` 特征的声明很简单:
```rust
unsafe trait Foo {
// 方法列表
@ -312,8 +337,8 @@ fn main() {}
总之,`Send` 特征标记为 `unsafe` 是因为 Rust 无法验证我们的类型是否能在线程间安全的传递,因此就需要通过 `unsafe` 来告诉编译器,它无需操心,剩下的交给我们自己来处理。
## 访问 union 中的字段
截止目前,我们还没有介绍过 `union` ,原因很简单,它主要用于跟 `C` 代码进行交互。
访问 `union` 的字段是不安全的,因为 Rust 无法保证当前存储在 `union` 实例中的数据类型。
@ -330,15 +355,16 @@ union MyUnion {
关于 `union` 的更多信息,可以在[这里查看](https://doc.rust-lang.org/reference/items/unions.html)。
## 一些实用工具(库)
由于 `unsafe``FFI` 在 Rust 的使用场景中是相当常见的(例如相对于 Go 的 `unsafe` 来说),因此社区已经开发出了相当一部分实用的工具,可以改善相应的开发体验。
由于 `unsafe``FFI` 在 Rust 的使用场景中是相当常见的(例如相对于 Go 的 `unsafe` 来说),因此社区已经开发出了相当一部分实用的工具,可以改善相应的开发体验。
#### rust-bindgen 和 cbindgen
对于 `FFI` 调用来说,保证接口的正确性是非常重要的,这两个库可以帮我们自动生成相应的接口,其中 [`rust-bindgen`](https://github.com/rust-lang/rust-bindgen) 用于在 Rust 中访问 C 代码,而 [`cbindgen`](https://github.com/eqrion/cbindgen/)则反之。
下面以 `rust-bindgen` 为例,来看看如何自动生成调用 C 的代码,首先下面是 C 代码:
```c
typedef struct Doggo {
int many;
@ -349,6 +375,7 @@ void eleven_out_of_ten_majestic_af(Doggo* pupper);
```
下面是自动生成的可以调用上面代码的 Rust 代码:
```rust
/* automatically generated by rust-bindgen 0.99.9 */
@ -364,9 +391,11 @@ extern "C" {
```
#### cxx
如果需要跟 C++ 代码交互,非常推荐使用 [`cxx`](https://github.com/dtolnay/cxx),它提供了双向的调用,最大的优点就是安全:是的,你无需通过 `unsafe` 来使用它!
#### Miri
[`miri`](https://github.com/rust-lang/miri) 可以生成 Rust 的中间层表示 MIR对于编译器来说我们的 Rust 代码首先会被编译为 MIR ,然后再提交给 LLVM 进行处理。
可以通过 `rustup component add miri` 来安装它,并通过 `cargo miri` 来使用,同时还可以使用 `cargo miri test` 来运行测试代码。
@ -378,23 +407,26 @@ extern "C" {
- 数据竞争
- 内存对齐问题
但是需要注意的是,它只能帮助识别被执行代码路径的风险,哪些未被执行到的代码是没办法被识别的。
#### Clippy
官方的 [`clippy`](https://github.com/rust-lang/rust-clippy) 检查器提供了有限的 `unsafe` 支持,虽然不多,但是至少有一定帮助。例如 `missing_safety_docs` 检查可以帮助我们检查哪些 `unsafe` 函数遗漏了文档。
需要注意的是: Rust 编译器并不会默认开启所有检查,大家可以调用 `rustc -W help` 来看看最新的信息。
#### Prusti
[`prusti`](https://viperproject.github.io/prusti-dev/user-guide/) 需要大家自己来构建一个证明,然后通过它证明代码中的不变量是正确被使用的,当你在安全代码中使用不安全的不变量时,就会非常有用。具体的使用文档见[这里](https://viperproject.github.io/prusti-dev/user-guide/)。
#### 模糊测试(fuzz testing)
在 [Rust Fuzz Book](https://rust-fuzz.github.io/book/) 中列出了一些 Rust 可以使用的模糊测试方法。
同时,我们还可以使用 [`rutenspitz`](https://github.com/jakubadamw/rutenspitz) 这个过程宏来测试有状态的代码,例如数据结构。
## 总结
至此,`unsafe` 的五种兵器已介绍完毕,大家是否意犹未尽?我想说的是,就算意犹未尽,也没有其它武器了。
就像上一章中所提到的,`unsafe` 只应该用于这五种场景,其它场景,你应该坚决的使用安全的代码,否则就会像 `actix-web` 的前作者一样,被很多人议论,甚至被喷。。。
@ -402,4 +434,6 @@ extern "C" {
总之,能不使用 `unsafe` 一定不要使用,就算使用也要控制好边界,让范围尽可能的小,就像本章的例子一样,只有真的需要 `unsafe` 的代码,才应该包含其中, 而不是将无关代码也纳入进来。
## 进一步学习
1. [Unsafe Rust: How and when (not) to use it](https://blog.logrocket.com/unsafe-rust-how-and-when-not-to-use-it/)
1. [Unsafe Rust: How and when (not) to use it](https://blog.logrocket.com/unsafe-rust-how-and-when-not-to-use-it/)

Loading…
Cancel
Save