【问题标题】:What is the idiomatic way to write a linked list with a tail pointer?用尾指针编写链表的惯用方法是什么?
【发布时间】:2015-06-15 04:20:20
【问题描述】:

作为 Rust 的一个学习项目,我有一个非常简单的(工作,如果不完整的话)单链表实现。结构的声明如下所示:

type NodePtr<T> = Option<Box<Node<T>>>;

struct Node<T> {
    data: T,
    next: NodePtr<T>,
}

pub struct LinkedList<T> {
    head: NodePtr<T>,
}

实现 sizepush_front 都相当简单,尽管迭代地执行大小确实涉及一些“与借用检查器的斗争”。

接下来我想尝试的是添加一个指向LinkedList 结构的tail 指针。启用高效的push_back 操作。在这里,我遇到了一点墙。起初我尝试使用Option&lt;&amp;Box&lt;Node&lt;T&gt;&gt;&gt;,然后是Option&lt;&amp;Node&lt;T&gt;&gt;。这两种情况都导致'as 到处都是,但最终仍然无法向生命周期检查器保证tail 是有效的。

从那以后,我得出的初步结论是,这些定义是不可能的:没有办法向编译器保证tail 在我认为的地方有效已验证。我可以做到这一点的唯一方法是让我的所有指针都是Rc&lt;_&gt;Rc&lt;RefCell&lt;_&gt;&gt;,因为这是让两个东西指向同一个对象(最终节点)的唯一安全方法。

我的问题:这是正确的结论吗?更一般地说:对于数据结构中的无主指针,惯用的 Rust 解决方案是什么?在我看来,对于如此简单的事情,引用计数似乎非常重要,所以我想我一定遗漏了一些东西。 (或者也许我只是还没有进入正确的记忆安全心态。)

【问题讨论】:

  • 你是对的;如果没有像Rc 这样的东西所提供的共享所有权,就不可能在安全的 Rust 中表达循环。
  • 这看起来是我问题的正确答案,但我也不想共享所有权。我想除了Rc 之外,我还会玩std::rc::Weak,因为我刚刚注意到前者存在。感谢您的快速回复!
  • 在我看来,对于如此简单的事情,引用计数似乎非常重要 =>当您开始关注所有权问题(谁拥有每条数据)时,你会意识到你对简单的印象是错误的:)
  • 错误是一个强词;也许不同?这是一个棘手的陈述,因为在单链表中维护尾指针上的不变量非常非常容易——这是一个常见的高中 CS 主题。从数学上证明它们被维护是一个不同的问题。

标签: linked-list rust reference-counting


【解决方案1】:

是的,如果你想写一个带有尾指针的单链表,你有三个选择:

  • 安全且可变:使用 NodePtr = Option&lt;Rc&lt;RefCell&lt;Node&lt;T&gt;&gt;&gt;&gt;
  • 安全且不可变:使用 NodePtr = Option&lt;Rc&lt;Node&lt;T&gt;&gt;&gt;
  • 不安全和可变:使用tail: *mut Node&lt;T&gt;

*mut 会更有效率,而且Rc 实际上不会防止你产生完全无意义的状态(正如你正确推断的那样)。它只是要保证它们不会导致段错误(尽管使用 RefCell 它可能仍会导致运行时崩溃......)。

最终,任何比普通单链表更复杂的链表都有一个所有权故事,它过于复杂,无法在 Rust 的所有权系统中安全有效地编码(它不是一棵树)。我个人更倾向于在那个时候接受不安全性并依靠单元测试来一次完成(为什么要编写一个次优的数据结构......?)。

【讨论】:

  • 这听起来肯定是一个引导性的问题,但我向你保证,我真的在努力理解 Rust 的精神。如果我被要求“接受不安全”以编写非平凡的非树数据结构,我为什么不坚持使用我已经有多年经验的 C++?是否暗示大多数好的数据结构应该是树?还是我错过了故事的其他部分?
  • 我认为编写集合不是大多数程序会费心去做的事情。他们将获得预先制作的数据结构(可能只是来自 std)。集合从根本上说也是一个低级构造。您需要直接与系统分配器对话,使用部分初始化的数据,并维护复杂的不变量。特别是如果你想要表现。一旦你建立了你的收藏,它应该公开一个完全安全的界面,没有人需要关心内部。 Rust 也给基础库带来了更大的负担。应用程序从安全中获得更大的胜利。
  • 至于大多数好的数据结构是树:不,大多数好的数据结构是数组。 :)
  • 关于“为什么不只是 C++”的注释:Rust 的安全性是模块化的和可选择退出的。例如,当您选择使用未初始化的内存时,您不必突然担心空指针(无论如何,Safe Rust 实际上也有非常棒的 100% 安全的方法来使用未初始化的内存)。此外,根据您正在处理的代码类型,您花费 100% 到 90% 的时间根本不用担心不安全性。 C++?不安全无处不在。 (刚刚意识到您可能不会收到这些回复,所以请抄送@GrandOpener)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-10-27
相关资源
最近更新 更多