布局
那么单链队列是什么样的呢?当我们有一个单链接的堆栈时,我们把它推到列表的一端,然后从同一端弹出。堆栈和队列之间的唯一区别是队列从另一端弹出。因此,从我们的堆栈实现中,我们可以看到:
input list:
[Some(ptr)] -> (A, Some(ptr)) -> (B, None)
stack push X:
[Some(ptr)] -> (X, Some(ptr)) -> (A, Some(ptr)) -> (B, None)
stack pop:
[Some(ptr)] -> (A, Some(ptr)) -> (B, None)为了建立队列,我们只需要决定移动到列表末尾的操作:push还是pop?因为我们的列表是单链的,我们实际上可以用同样的工作方式将任何一个操作移到最后。
要将 push 移动到结尾,我们只需要一直走到 None,并将新元素将其设置为 Some。
input list:
[Some(ptr)] -> (A, Some(ptr)) -> (B, None)
flipped push X:
[Some(ptr)] -> (A, Some(ptr)) -> (B, Some(ptr)) -> (X, None)要将pop移到末尾,我们一直走到None之前的节点,然后取走:
input list:
[Some(ptr)] -> (A, Some(ptr)) -> (B, Some(ptr)) -> (X, None)
flipped pop:
[Some(ptr)] -> (A, Some(ptr)) -> (B, None)我们今天就可以做到这一点,然后结束。但那会很糟糕!这两个操作都会遍历整个列表。有些人会认为这样的队列实现实际上是一个队列,因为它公开了正确的接口。不过,我保证接口的性能。我不关心精确的渐近界,只关心“快”与“慢”。队列保证了push和pop的速度很快,而遍历整个列表绝对不是很快。
一个关键的观察是,我们重复做同样的事情浪费了大量的工作。我们能记住这个工作吗?为什么,是的!我们可以存储一个指向列表末尾的指针,然后直接跳到那里!
事实证明,只有 push 和 pop 的一种反转才可以使用。 要反转 pop,我们必须将 “tail” 指针向后移动,但是由于我们的列表是单链链接,因此我们无法有效地做到这一点。 如果我们取反推,则只需向前移动 “head” 指针,这很容易。
让我们试试这个:
我现在将更详细地介绍隐含的细节,因为我们应该对这种事情很满意。 并不是说您一定一定希望在第一次尝试时就产生此代码。 我只是略过了我们之前必须处理的反复试验。 实际上,在编写此代码时我犯了很多错误,但我没有显示出来。 您只能看到我离开 mut 或 ; 如此反复才停止启发性。 不用担心,我们还会看到许多其他错误消息!
使用 move value: new tail
没有实现 Copy,所以我们不能把它分配到两个位置。更重要的是,Box 拥有它所指向的东西,并且会在它 drop 时释放它。如果我们编译了 push 实现,我们就可以双倍释放列表的尾部了!实际上,正如我们所写的那样,我们的代码会在每次推送时释放 old_tail。哎呀!
好吧,我们知道如何制作一个非所有权指针。 那只是参考!
这里没有什么棘手的问题。 与之前的代码相同的基本思想,不同之处在于,无论使用什么填充实际Box的位置,我们都使用一些隐式的返回善意来提取尾部引用。
哦,对了,我们需要在类型生命周期中提供引用。 嗯...这个引用的生命周期是多少? 好吧,这看起来像IterMut,对吗? 让我们尝试一下为IterMut做的事情,然后添加一个通用的 'a:
哇,这是一个非常详细的错误消息。这有点令人担忧,因为这意味着我们正在做一些真正混乱的事情。这里有一个有趣的部分:
必须在 impl 上定义有效生命周期 'a
我们借用了self,但是编译器希望我们能持续到 'a,如果我们告诉它self能持续那么长时间呢。。?
哦,嘿,成功了! 太好了!
让我们也做一下 pop 吧:
并为此写一个快速测试:
🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀
我的天啊。
编译器对我们所有人的呕吐没有错。我们刚犯了一个主要的Rust罪过:我们在自己的内部存储了对自己的引用。 不知何故,我们设法说服了Rust,这在我们的push和pop实施中是完全合理的(我们确实为之震惊)。 我相信原因是Rust不能仅仅通过 push 和 pop 来告诉引用是对我们自己的引用,或者更确切地说,Rust根本没有这个概念。 引用失败的行为只是一种新出现的行为。
一旦我们尝试使用我们的列表,一切都会很快崩溃。 当我们调用push或pop时,我们会立即在自己中存储对自己的引用并陷入困境。 我们实际上是在借用自己。
我们的 pop 实现提示了为什么这可能是非常危险的:
如果我们忘记这样做怎么办? 然后,我们的尾巴将指向已从列表中删除的某个节点。 这样的节点将立即释放,并且我们将有一个悬空的指针,Rust应该保护它免受攻击!
的确,Rust 正在保护我们远离那种危险,只是以一种非常... 迂回的方式。
那么我们能做什么呢? 回到 Rc<refcell> 地狱?
求你了不要。
相反,我们将偏离正轨,使用原始指针。我们的布局将是这样的:
就是这样。 这些无聊的引用计数动态借阅检查都是胡说八道! 真实。 硬。 未检查的指针。
让我们成为C。 让我们整天成为C。
我回来了,我准备好了。
Hello unsafe。
Last updated
Was this helpful?