#tree #strategies #“dom-like”

forest

Rust 中“DOM-like”树数据结构的各种实现策略

1 个不稳定版本

使用旧的 Rust 2015

0.0.1 2015年4月10日

#26#strategies

1KB

不打算维护

No Maintenance Intended

此存储库存在是为了发布我在某个周末进行的一些实验的结果。这不是一个随着时间的推移而被维护的软件项目。

如果您想使用此代码,请考虑将其复制到自己的源存储库中,并遵守MIT许可条款。

rust-forest

Rust 中“DOM-like”树数据结构的各种实现策略。

在这里,“DOM-like”意味着可以使用数据结构来表示HTML或XML文档的解析内容,就像DOM所做的那样,但不必具有与DOM完全相同的API。也就是说

  • 树由节点组成。
  • 每个节点可以有零个或多个 子节点,这些节点是有序的。
  • 每个节点最多有一个 父节点,即它是其 子节点
  • 没有 父节点 的节点被称为 根节点
  • 因此,每个节点也可能有 兄弟节点:如果有的话,它的 父节点 的其他 子节点
  • 从任何给定的节点,访问其父节点、前一个兄弟节点、后一个兄弟节点、第一个子节点和最后一个子节点(如果有),最多需要 O(1) 时间。
  • 每个节点还关联着一些数据,对于本项目来说,这些数据是纯泛型的。对于HTML文档,数据可以是文本节点的文本,或者是元素节点的名称和属性。
  • 树是可变的:节点(及其子树)可以在树的任何位置插入或删除。

特别是,需要访问父节点意味着我们不能使用明显的结构,即子节点被父节点直接 拥有

struct Node<T> {
    data: T,
    children: Vec<Node<T>>,
    parent: /* ??? */
}

更普遍地,具有父子关系和兄弟关系(除子关系外)的树可以被视为一种特殊类型的图,但仍然是一个包含环的图。而Rust的默认所有权模型并不容易支持环。因此,我们需要更复杂的策略来管理节点的生命周期。

Servo 中,DOM节点由JavaScript垃圾回收器管理。然而,Rust目前还没有自己的跟踪垃圾回收器,并且许多Rust项目可能希望操作DOM-like树而不嵌入整个JavaScript虚拟机。

此存储库包含多个crate,每个crate都使用不同的方法实现不同的权衡来实现DOM-like数据结构。

rctree

节点的生命周期通过 引用计数 来管理。为了避免导致内存泄漏的引用循环,树是 不对称的:每个节点持有对下一个兄弟节点和第一个子节点的可选 强引用,但只对父节点、前一个兄弟节点和最后一个子节点持有可选的 弱引用

节点一旦没有强引用,就会被销毁。结构是这样的,只要持有根节点的引用,就可以保持整棵树的生命。然而,如果从树外部的唯一引用是用来遍历树的,那么在向下遍历之后,将无法回到树上的祖先和之前的兄弟节点,因为这些节点已经被销毁了。

对已销毁节点的弱引用被视为未设置。例如,当其父节点被销毁时,节点可以成为根节点。

由于节点被别名(存在多个引用),所以使用RefCell来实现内部可变。

优势

  • 有一个可见的NodeRef类型来操作树,并具有相应的方法
  • 内存一旦不再使用就会被释放(如果删除树的某些部分)。

劣势

  • 树只能从创建它的线程中访问。
  • 任何树操作,包括只读遍历,都需要增加和减少引用计数,这会导致运行时开销
  • 节点单独分配,这可能会导致内存碎片化并影响性能。

arena-tree

节点生命周期通过场分配器进行管理。

节点通过&'a T引用与场的生命周期相关联,当场被销毁时,节点将同时被销毁。节点之间的链接在内部也是&'a T引用。

由于节点被别名(存在多个引用),所以使用Cell来实现内部可变。

优势

  • 节点仍然有方法,在树操作过程中可以大致忽略场(只要它保持存活)。
  • 运行时开销更小。(分配速度快,不需要增加或减少引用计数。)
  • 节点在内存中分配成大块连续的内存。这有助于提高内存缓存性能。

劣势

  • 还有一个对象(场)需要保留。
  • 树只能从创建它的线程中访问。
  • 当节点在场被销毁之前从树中移除时,会发生内存“泄漏”。可以通过维护一个空闲列表来缓解这个问题,该列表包含不再使用的节点,并且可以被重用,但前提是这些节点已经被显式释放。(而引用计数和垃圾回收会自动释放未使用的节点。)但这样并不能阻止保留对已释放节点的引用。可以添加另一个显式机制(例如在每个节点中跟踪一个生成号),以防止(主要是)此类引用和访问已释放或重新分配的节点。

idtree

arena-tree类似,但场被简化为单个Vec,并使用数值标识符(向量中的索引)而不是&'a T引用。

优势

  • 没有RefCell,可变性的处理方式更符合Rust的惯例,即通过唯一的(&mut)访问场。
  • 树可以像Vec一样发送或跨线程共享。这允许进行并行树遍历,例如。

劣势

  • 每次访问都需要通过场进行,这可能很麻烦。
  • 由于边界检查,与arena-tree相比,存在一些运行时开销。
  • 来自给定场的节点ID可以在不同的场中使用,这不太可能导致错误并访问无关的节点。

无运行时依赖