You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

91 lines
6.7 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

## 我们到底需不需要链表
经常有读者询问该如何实现一个链表,怎么说呢,这个答案主要取决于你的需求,因此并不是很好回答。鉴于此,我决定通过这本书来详尽的介绍该如何实现一个链表,大家应该都能从这本书中找到答案。
书中我们将通过实现 6 种链表来学习基本和进阶 Rust 编程知识,在此过程中,你能学到:
- 指针类型: `&`, `&mut`, `Box`, `Rc`, `Arc`, `*const`, `*mut`, `NonNull`
- 所有权、借用、继承可变性、内部可变性、Copy
- 所有的关键字struct、enum、fn、pub、impl、use, ...
- 模式匹配、泛型、解构
- 测试、安装新的工具链、使用 `miri`
- Unsafe: 裸指针、别名、栈借用、`UnsafeCell`、变体 variance
是的,链表就是这么可怕,只有将这些知识融会贯通后,你才能掌握 :(
> 事实上这本书中关于 Rust 语言的绝大部分知识都在 `Rust语言圣经`中有讲,因此除非特殊情况,我们将直接提供链接供大家学习,重点还是放在链表实现上
#### 创建一个项目
在开始前,先来创建一个项目专门用于链表学习:
```shell
> cargo new --lib lists
> cd lists
```
之后,我们会将每个一个链表放入单独的文件中,需要注意的是我们会尽量模拟真实的 Rust 开发场景:你写了一段代码,然后编译器开始跳出试图教你做事,只有这样才能真正学会 Rust温室环境是无法培养出强大的 Rustacean 的。
#### 义务告知
首先,本书不是保姆式教程,而且我个人认为编程应该是快乐,这种快乐往往需要你自己发现而不是别人的事无巨细的讲解。
其次,我讨厌链表。链表真的是一种糟糕的数据结构,尽管它在部分场景下确实很有用:
- 对列表进行大量的分割和合并操作
- 无锁并发
- 要实现内核或嵌入式的服务
- 你在使用一个纯函数式语言,由于受限的语法和缺少可变性,因此你需要使用链表来解决这些问题
但是实事求是的说,这些场景对于几乎任何 Rust 开发都是很少遇到的99% 的场景你可以使用 `Vec` 来替代,然后 1% 中的 99% 可以使用 `VecDeque`。 由于它们具有更少的内存分配次数、更低的内存占用、随机访问和缓存亲和特性,因此能够适用于绝大多数工作场景。总之,类似于 `trie` 树,链表也是一种非常小众的数据结构,特别是对于 Rust 开发而言。
> 本书只是为了学习链表该如何实现,如果大家只是为了使用链表,强烈推荐直接使用标准库或者社区提供的现成实现,例如 [std::collections::LinkedList](https://doc.rust-lang.org/std/collections/struct.LinkedList.html)
#### 链表有 O(1) 的分割、合并、插入、移除性能
是的,但是你首先要考虑的是,这些代码被调用的频率是怎么样的?是否在热点路径? 答案如果是否定的,那么还是强烈建议使用 `Vec` 等传统数据结构,况且整个数组的拷贝也是相当快的!
况且,`Vec` 上的 `push``pop` 操作是 `O(1)` 的,它们比链表提供的 `push``pop` 要更快!我们只需要通过一个指针 + 内存偏移就可以访问了。
> 关于是否使用链表这个问题Bjarne Stroustrup 有过非常深入的[讲解](https://www.youtube.com/watch?v=YQs6IC-vgmo)
但是如果你的整体项目确实因为某一段分割、合并的代码导致了性能低下,那么就放心大胆的使用链表吧。
#### 我无法接受内存重新分配的代价
是的,`Vec` 当 [`capacity`](https://practice.rs/collections/vector.html#capacity) 不够时,会重新分配一块内存,然后将之前的 `Vec` 全部拷贝过去,但是对于绝大多数使用场景,要么 `Vec` 不在热点路径中,要么 `Vec` 的容量可以提前预测。
对于前者,那性能如何自然无关紧要。而对于后者,我们只需要使用 `Vec::with_capacity` 提前分配足够的空间即可同时Rust 中所有的迭代器还提供了 `size_hint` 也可以解决这种问题。
当然,如果这段代码在热点路径,且你无法提前预测所需的容量,那么链表确实会更提升性能。
#### 链表更节省内存空间
首先,这个问题较为复杂。一个标准的数组调整策略是:增加或减少数组的长度使数组最多有一半为空,例如 capacity 增长是翻倍的策略。这确实会导致内存空间的浪费,特别是在 Rust 中,我们不会自动收缩集合类型。
但是上面说的是最坏的情况,如果是最好的情况,那整个数组其实只有 3 个指针大小(指针在 Rust 中占用一个 word 的空间,例如 64 位机器就是 8 个字节的大小)的内存浪费,或者说,没有浪费。
而且链表实际上也有内存浪费,例如链表中的每个元素都会占用额外的内存:单向链表浪费一个指针,双向链表浪费两个指针。当然,如果你的链表中每个元素都很大,那相对来说,这种浪费也微不足道,但是如果链表的元素较小且数量很多呢?那浪费的空间就相当可观了!
当然,这个也和使用的内存分配器有关( allocator ):对链表节点的分配和回收会经常发生,这样就不会浪费内存。
总之,如果链表的元素较大,你也无法预测数组的空间,同时还有一个不错的内存分配器,那链表确实可以节省空间!
#### 我在函数语言中一直使用链表
对于函数语言而言,链表确实非常棒,因为你可以解决可变性问题,还能递归地去使用,当然,可能还有一定的图方便的因素,因为链表不用操心长度等问题。
但彼之蜜糖不等于吾之蜜糖,函数语言的一些使用习惯不应该带入到其它语言中,例如 Rust。
- 函数语言往往将链表用于迭代,但是 Rust 中最适合迭代的数据结构是迭代器 `Iterator`
- 函数式语言的不可变对于 Rust 也不是问题
- Rust 还支持对数组进行切片以获取其中一部分连续的元素,而在函数语言中你可能得通过链表的 `head/tail` 分割来完成
其实,在函数语言中,我们也应该选择合适的数据结构来解决适合的场景,而不是*一根链表挂腰间,潇潇洒洒走天下*。
#### 链表适合构建并发数据结构
是这样的,如果有这样的需求,那么链表会非常合适!但是只有在你确实需要并发数据结构,且没有其它办法时,再考虑链表!
#### 链表非常适合教学目的
额... 这么说也没错,毕竟所有的编程语言课程都以链表来作为最常见的练手项目,包括本书也是服务于这个目的的。