diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 290efc1b..cfc7e5fe 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -167,7 +167,8 @@ - [持久化单向链表](too-many-lists/persistent-stack/intro.md) - [数据布局和基本操作](too-many-lists/persistent-stack/layout.md) - [Drop、Arc 及完整代码](too-many-lists/persistent-stack/drop-arc.md) - + - [不咋样的双端队列](too-many-lists/deque/intro.md) + - [数据布局和基本操作](too-many-lists/deque/layout.md) - [易混淆概念解析](confonding/intro.md) - [切片和切片引用](confonding/slice.md) - [Eq 和 PartialEq](confonding/eq.md) @@ -241,7 +242,7 @@ - [HashMap todo](std/hashmap.md) - [Iterator 常用方法 todo](std/iterator.md) -- [Ctrl-C/V: 编程常用代码片段 todo](cases/intro.md) +- [CookBook](cases/intro.md) - [命令行解析 todo](cases/cmd.md) - [配置文件解析 todo](cases/config.md) - [编解码 todo](cases/encoding/intro.md) diff --git a/src/cases/intro.md b/src/cases/intro.md index c6b7d124..0e1d78aa 100644 --- a/src/cases/intro.md +++ b/src/cases/intro.md @@ -1 +1,3 @@ # 场景化用例 + +https://rust-lang-nursery.github.io/rust-cookbook/ \ No newline at end of file diff --git a/src/too-many-lists/deque/intro.md b/src/too-many-lists/deque/intro.md new file mode 100644 index 00000000..44e05eef --- /dev/null +++ b/src/too-many-lists/deque/intro.md @@ -0,0 +1,16 @@ +# 不太优秀的双端队列 +在实现了之前的队列后,我们不禁浮想联翩,如果 `Rc` 是可变的,那是不是可以实现一个双向链表? + +心动不如行动,先来创建新的链表文件 `fourth.rs`,并在 `src/lib.rs` 中添加以下内容: +```rust +// in lib.rs + +pub mod first; +pub mod second; +pub mod third; +pub mod fourth; +``` + +依然是熟悉的从零开始,当然,也依然会用到熟悉的 CV 配方。 + +> 声明:大家看到目录名时,心里就应该在嘀咕了吧?其实你的嘀咕是对的,是的,本章的目的是为了证明之前的想法是糟糕的! \ No newline at end of file diff --git a/src/too-many-lists/deque/layout.md b/src/too-many-lists/deque/layout.md new file mode 100644 index 00000000..b58edc99 --- /dev/null +++ b/src/too-many-lists/deque/layout.md @@ -0,0 +1,316 @@ +# 数据布局和构建 +聪明的读者应该已经想到了:让 `Rc` 可变,就需要使用 `RefCell` 的配合。关于 `RefCell` 的一切,在之前的章节都有介绍,还不熟悉的同学请移步[这里](https://course.rs/advance/smart-pointer/cell-refcell.html)。 + +好了,绝世神兵在手,接下来...我们将见识一个绝世啰嗦的数据结构...如果你来自 GC 语言,那很可能就没有见识过这种阵仗。 + +## 数据布局 + +双向链表意味着每一个节点将同时指向前一个和下一个节点,因此我们的数据结构可能会变成这样: +```rust +use std::rc::Rc; +use std::cell::RefCell; + +pub struct List { + head: Link, + tail: Link, +} + +type Link = Option>>>; + +struct Node { + elem: T, + next: Link, + prev: Link, +} +``` + +耳听忐忑,心怀忐忑,尝试编译下,竟然顺利通过了,thanks god! 接下来再来看看该如何使用它。 + +## 构建 +如果按照之前的构建方式来构建新的数据结构,会有点笨拙,因此我们先尝试将其拆分: +```rust +impl Node { + fn new(elem: T) -> Rc> { + Rc::new(RefCell::new(Node { + elem: elem, + prev: None, + next: None, + })) + } +} + +impl List { + pub fn new() -> Self { + List { head: None, tail: None } + } +} +``` + +```rust +> cargo build + +**一大堆 DEAD CODE 警告,但是好歹可以成功编译** +``` + +## Push +很好,再来向链表的头部推入一个元素。由于双向链表的数据结构和操作逻辑明显更加复杂,因此相比单向链表的单行实现,双向链表的 `push` 操作也要复杂的多。 + +除此之外,我们还需要处理一些关于空链表的边界问题:对于绝大部分操作而言,可能只需要使用 `head` 或 `tail` 指针,但是对于空链表,则需要同时使用它们。 + +一个验证方法 `methods` 是否有效的办法就是看它是否能保持不变性, 每个节点都应该有两个指针指向它: 中间的节点被它前后的节点所指向,而头部的端节点除了被它后面的节点所指向外,还会被链表本身所指向,尾部的端节点亦是如此。 + +```rust +pub fn push_front(&mut self, elem: T) { + let new_head = Node::new(elem); + match self.head.take() { + Some(old_head) => { + // 非空链表,将新的节点跟老的头部相链接 + old_head.prev = Some(new_head.clone()); + new_head.next = Some(old_head); + self.head = Some(new_head); + } + None => { + // 空链表,需要设置 tail 和 head + self.tail = Some(new_head.clone()); + self.head = Some(new_head); + } + } +} +``` + +```rust +cargo build + +error[E0609]: no field `prev` on type `std::rc::Rc>>` + --> src/fourth.rs:39:26 + | +39 | old_head.prev = Some(new_head.clone()); // +1 new_head + | ^^^^ unknown field + +error[E0609]: no field `next` on type `std::rc::Rc>>` + --> src/fourth.rs:40:26 + | +40 | new_head.next = Some(old_head); // +1 old_head + | ^^^^ unknown field +``` + +虽然有报错,但是一切尽在掌握,今天真是万事顺利啊! + +从报错来看,我们无法直接去访问 `prev` 和 `next`,回想一下 `RefCell` 的使用方式,修改代码如下: +```rust +pub fn push_front(&mut self, elem: T) { + let new_head = Node::new(elem); + match self.head.take() { + Some(old_head) => { + old_head.borrow_mut().prev = Some(new_head.clone()); + new_head.borrow_mut().next = Some(old_head); + self.head = Some(new_head); + } + None => { + self.tail = Some(new_head.clone()); + self.head = Some(new_head); + } + } +} +``` + +```shell +> cargo build + +warning: field is never used: `elem` + --> src/fourth.rs:12:5 + | +12 | elem: T, + | ^^^^^^^ + | + = note: #[warn(dead_code)] on by default +``` + +嘿,我又可以了!既然状态神勇,那就趁热打铁,再来看看 `pop`。 + +## Pop +如果说 `new` 和 `push` 是在构建链表,那 `pop` 显然就是一个破坏者。 + +何为完美的破坏?按照构建的过程逆着来一遍就是完美的! +```rust +pub fn pop_front(&mut self) -> Option { + self.head.take().map(|old_head| { + match old_head.borrow_mut().next.take() { + Some(new_head) => { + // 非空链表 + new_head.borrow_mut().prev.take(); + self.head = Some(new_head); + } + None => { + // 空链表 + self.tail.take(); + } + } + old_head.elem + }) +} +``` + +```shell +> cargo build + +error[E0609]: no field `elem` on type `std::rc::Rc>>` + --> src/fourth.rs:64:22 + | +64 | old_head.elem + | ^^^^ unknown field +``` + +哎,怎么就不长记性呢,又是 `RefCell` 惹的祸: +```rust +pub fn pop_front(&mut self) -> Option { + self.head.take().map(|old_head| { + match old_head.borrow_mut().next.take() { + Some(new_head) => { + new_head.borrow_mut().prev.take(); + self.head = Some(new_head); + } + None => { + self.tail.take(); + } + } + old_head.borrow_mut().elem + }) +} +``` + +```shell +cargo build + +error[E0507]: cannot move out of borrowed content + --> src/fourth.rs:64:13 + | +64 | old_head.borrow_mut().elem + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot move out of borrowed content +``` + +额... 我凌乱了,看上去 `Box` 是罪魁祸首,`borrow_mut` 只能返回一个 `&mut Node`,因此无法拿走其所有权。 + +我们需要一个方法来拿走 `RefCell` 的所有权,然后返回给我们一个 `T`, 翻一翻[文档](https://doc.rust-lang.org/std/cell/struct.RefCell.html),可以发现下面这段内容: + +> `fn into_inner(self) -> T` + +> 消费掉 RefCell 并返回内部的值 + +喔,看上去好有安全感的方法: +```rust +old_head.into_inner().elem +``` + +```shell +> cargo build + +error[E0507]: cannot move out of an `Rc` + --> src/fourth.rs:64:13 + | +64 | old_head.into_inner().elem + | ^^^^^^^^ cannot move out of an `Rc` +``` + +...看走眼了,没想到你浓眉大眼也会耍花枪。 `into_inner` 想要拿走 `RecCell` 的所有权,但是还有一个 `Rc` 不愿意,因为 `Rc` 只能让我们获取内部值的不可变引用。 + +大家还记得我们之前实现 `Drop` 时用过的方法吗?在这里一样适用: +```rust +Rc::try_unwrap(old_head).unwrap().into_inner().elem +``` + +`Rc::try_unwrap` 返回一个 `Result`,由于我们不关心 `Err` 的情况( 如果代码合理,这里不会是 `Err` ),直接使用 `unwrap` 即可。 + +```shell +> cargo build + +error[E0599]: no method named `unwrap` found for type `std::result::Result>, std::rc::Rc>>>` in the current scope + --> src/fourth.rs:64:38 + | +64 | Rc::try_unwrap(old_head).unwrap().into_inner().elem + | ^^^^^^ + | + = note: the method `unwrap` exists but the following trait bounds were not satisfied: + `std::rc::Rc>> : std::fmt::Debug` +``` + +额,`unwrap` 要求目标类型是实现了 `Debug` 的,这样才能在报错时提供 `debug` 输出,而 `RefCell` 要实现 `Debug` 需要它内部的 `T` 实现 `Debug`,而我们的 `Node` 并没有实现。 + +当然,我们可以选择为 `Node` 实现,也可以这么做: +```rust +Rc::try_unwrap(old_head).ok().unwrap().into_inner().elem +``` + +```shell +cargo build +``` + +终于成功的运行了,下面依然是惯例 - 写几个测试用例 : +```rust +#[cfg(test)] +mod test { + use super::List; + + #[test] + fn basics() { + let mut list = List::new(); + + // Check empty list behaves right + assert_eq!(list.pop_front(), None); + + // Populate list + list.push_front(1); + list.push_front(2); + list.push_front(3); + + // Check normal removal + assert_eq!(list.pop_front(), Some(3)); + assert_eq!(list.pop_front(), Some(2)); + + // Push some more just to make sure nothing's corrupted + list.push_front(4); + list.push_front(5); + + // Check normal removal + assert_eq!(list.pop_front(), Some(5)); + assert_eq!(list.pop_front(), Some(4)); + + // Check exhaustion + assert_eq!(list.pop_front(), Some(1)); + assert_eq!(list.pop_front(), None); + } +} +``` + +```shell +cargo test + + Running target/debug/lists-5c71138492ad4b4a + +running 9 tests +test first::test::basics ... ok +test fourth::test::basics ... ok +test second::test::iter_mut ... ok +test second::test::basics ... ok +test fifth::test::iter_mut ... ok +test third::test::basics ... ok +test second::test::iter ... ok +test third::test::iter ... ok +test second::test::into_iter ... ok + +test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured +``` + +## Drop +在[循环引用章节](),我们介绍过 `Rc` 最怕的就是引用形成循环,而双向链表恰恰如此。因此,当使用默认的实现来 `drop` 我们的链表时,两个端节点会将各自的引用计数减少到 1, 然后就不会继续减少,最终造成内存泄漏。 + +所以,这里最好的实现就是将每个节点 `pop` 出去,直到获得 `None`: +```rust +impl Drop for List { + fn drop(&mut self) { + while self.pop_front().is_some() {} + } +} +``` + +细心的读者可能已经注意到,我们还未实现在链表尾部 `push` 和 `pop` 的操作,但由于所需的实现跟之前差别不大,因此我们会在后面直接给出,下面先来看看更有趣的。