From a69c999ff689180b3200f1d4a5c85e0f0a48f619 Mon Sep 17 00:00:00 2001 From: sunface Date: Tue, 8 Mar 2022 10:59:20 +0800 Subject: [PATCH] add too-many-lists/intro.md --- src/SUMMARY.md | 6 +-- src/linked-list/intro.md | 13 ----- src/too-many-lists/intro.md | 104 ++++++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 17 deletions(-) delete mode 100644 src/linked-list/intro.md create mode 100644 src/too-many-lists/intro.md diff --git a/src/SUMMARY.md b/src/SUMMARY.md index cd17fdc4..769deb7c 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -90,7 +90,6 @@ - [实践应用:多线程 Web 服务器 todo](advance/concurrency-with-threads/web-server.md) - [全局变量](advance/global-variable.md) - [错误处理](advance/errors.md) - - [Unsafe Rust](advance/unsafe/intro.md) - [五种兵器](advance/unsafe/superpowers.md) - [内联汇编 todo](advance/unsafe/inline-asm.md) @@ -116,7 +115,6 @@ - [一些疑难问题的解决办法](async/pain-points-and-workarounds.md) - [实践应用:Async Web 服务器](async/web-server.md) - [Tokio 使用指南](tokio/intro.md) - - [tokio 概览](tokio/overview.md) - [使用初印象](tokio/getting-startted.md) - [创建异步任务](tokio/spawning.md) @@ -131,7 +129,6 @@ - [异步跟同步共存](tokio/bridging-with-sync.md) - [Cargo 使用指南](cargo/intro.md) - - [上手使用](cargo/getting-started.md) - [基础指南](cargo/guide/intro.md) - [为何会有 Cargo](cargo/guide/why-exist.md) @@ -156,7 +153,8 @@ - [构建脚本 build.rs](cargo/reference/build-script/intro.md) - [构建脚本示例](cargo/reference/build-script/examples.md) -- [手把手带你实现链表 doing](linked-list/intro.md) +- [手把手带你实现链表 doing](too-many-lists/intro.md) + - [易混淆概念解析](confonding/intro.md) - [切片和切片引用](confonding/slice.md) - [Eq 和 PartialEq](confonding/eq.md) diff --git a/src/linked-list/intro.md b/src/linked-list/intro.md deleted file mode 100644 index a3b0e8e5..00000000 --- a/src/linked-list/intro.md +++ /dev/null @@ -1,13 +0,0 @@ -# 手把手带你实现链表 - -> 其它语言:兄弟,语言学了吗?来写一个链表证明你基本掌握了语法。 -> -> Rust 语言: 兄弟,语言精通了吗?来写一个链表证明你已经精通了 Rust! - - -上面的对话非常真实,我们在之前的章节也讲过[初学者学习 Rust 应该避免的坑](https://course.rs/sth-you-should-not-do.html#千万别从链表或图开始练手),其中最重要的就是 - 不要写链表或者类似的数据结构! - -而本章,你就将见识到何为真正的深坑,看完后,就知道没有提早跳进去是一个多么幸运的事。总之,在专题中,你将学会如何使用 Rust 来实现链表。 - - -**专题内容翻译自英文开源书 [Learning Rust With Entirely Too Many Linked Lists](https://rust-unofficial.github.io/too-many-lists/),但是在内容上做了一些调整,希望大家喜欢。** \ No newline at end of file diff --git a/src/too-many-lists/intro.md b/src/too-many-lists/intro.md new file mode 100644 index 00000000..ca22f2d7 --- /dev/null +++ b/src/too-many-lists/intro.md @@ -0,0 +1,104 @@ +# 手把手带你实现链表 + +> 其它语言:兄弟,语言学了吗?来写一个链表证明你基本掌握了语法。 +> +> Rust 语言: 兄弟,语言精通了吗?来写一个链表证明你已经精通了 Rust! + + +上面的对话非常真实,我们在之前的章节也讲过[初学者学习 Rust 应该避免的坑](https://course.rs/sth-you-should-not-do.html#千万别从链表或图开始练手),其中最重要的就是 - 不要写链表或者类似的数据结构! + +而本章,你就将见识到何为真正的深坑,看完后,就知道没有提早跳进去是一个多么幸运的事。总之,在专题中,你将学会如何使用 Rust 来实现链表。 + + +**专题内容翻译自英文开源书 [Learning Rust With Entirely Too Many Linked Lists](https://rust-unofficial.github.io/too-many-lists/),但是在内容上做了一些调整(原书虽然非常棒,但是在一些内容组织和文字细节上我觉得还是可以优化下的 :D),希望大家喜欢。** + +## 本书的使用姿势 +经常有读者询问该如何实现一个链表,怎么说呢,这个答案主要取决于你的需求,因此并不是很好回答。鉴于此,我决定通过这本书来详尽的介绍该如何实现一个链表,大家应该都能从这本书中找到答案。 + +书中我们将通过实现 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` 分割来完成 + + +其实,在函数语言中,我们也应该选择合适的数据结构来解决适合的场景,而不是*一根链表挂腰间,潇潇洒洒走天下*。 + + +#### 链表适合构建并发数据结构 +是这样的,如果有这样的需求,那么链表会非常合适!但是只有在你确实需要并发数据结构,且没有其它办法时,再考虑链表! + +#### 链表非常适合教学目的 +额... 这么说也没错,毕竟所有的编程语言课程都以链表来作为最常见的练手项目,包括本书也是服务于这个目的的。 + + +