如何创建一个完全不可变的树层次结构?建筑鸡和蛋

Posted

技术标签:

【中文标题】如何创建一个完全不可变的树层次结构?建筑鸡和蛋【英文标题】:How to create a completely immutable tree hierarchy? Construction chicken and egg 【发布时间】:2012-02-16 01:52:08 【问题描述】:

我喜欢使数据类不可变来简化并发编程。但是制作一个完全不可变的层次结构似乎有问题。

考虑这个简单的树类:

public class SOTree 
    private final Set<SOTree> children = new HashSet<>();
    private SOTree parent;

    public SOTree(SOTree parent) 
        this.parent = parent;
    

    public SOTree(Set<SOTree> children) 
        for (SOTree next : children)
            children.add(next);
    


    public Set<SOTree> getChildren() 
        return Collections.unmodifiableSet(children);
    

    public SOTree getParent() 
        return parent;
    

现在,如果我想创建这些层次结构,当我构造它时,要么父节点必须存在于当前节点之前,要么子节点必须先存在。

    SOTree root = new SOTree((SOTree)null);
    Set<SOTree> children = createChildrenSomehow(root);
    //how to add children now?  or children to the children?

    Set<SOTree> children = createChildrenSomehow(null);
    SOTree root = new SOTree(children);
    //how to set parent on children?

在不强制它成为单链树的情况下,是否有任何巧妙的方法来构造这样一棵树并且仍然让所有节点完全不可变?

【问题讨论】:

为什么要避免单链树?您的“双向链接树”违反 DRY(不要重复自己),即您有数据重复。 @toto2:使用双链树,一些操作更容易。在遍历一棵树的过程中,有必要将从当前节点到根的路径存储在某处。根据树的使用方式,最好将每个节点返回到根的路径存储在树中,而不是要求每个遍历它的人在遍历它时生成他们自己的信息副本。 @supercat 好的,但这不是上面的反问:我想知道他是否真的需要双链树。如果没有,一切都会简单得多。 【参考方案1】:

Eric Lippert 最近在博客中讨论了这个问题。请参阅他的博客文章Persistence, Facades and Roslyn's Red-Green Trees。摘录如下:

我们实际上是通过保留 两个 解析树来完成不可能的事情。 “绿色”树是不可变的、持久的、没有父引用、是“自下而上”构建的,每个节点都跟踪它的 width 而不是它的绝对位置。当编辑发生时,我们只重建受编辑影响的绿树部分,这通常约为树中所有解析节点的 O(log n)。

“红”树是围绕绿树构建的不可变外观;它是“自上而下”按需构建的,并且在每次编辑时都被丢弃。它通过在您从顶部向下穿过树时按需制造父引用来计算父引用。再次,当你下降时,它通过计算宽度来制造绝对位置。

【讨论】:

【参考方案2】:

两个想法:

    使用某种树工厂。您可以使用可变结构来描述树,然后拥有一个组装不可变树的工厂。在内部,工厂可以访问不同节点的字段,因此可以根据需要重新连接内部指针,但生成的树将是不可变的。

    围绕可变树构建不可变树包装器。也就是说,让树构造使用可变节点,然后构建一个包装器类,然后提供树的不可变视图。这类似于 (1),但没有明确的工厂。

希望这会有所帮助!

【讨论】:

我最终使用构建器模式来描述树结构,然后在 build() 上创建树。效果很好。谢谢。 选项 3 是数字 2 的一个轻微变体,实现类似于 Freezable 模式的东西,其中对象是可变的,直到您调用 freeze(),然后它不是。【参考方案3】:

您已正确地将您的问题描述为鸡和蛋之一。另一种重申可能解决问题的方法是,您想要种一棵树(树根、树干、叶子和所有东西 - 一次全部)。

一旦你接受计算机只能一步一步地处理事物,一系列可能的解决方案就会出现:

    看看 Clojure 如何创建不可变数据结构。在 Clojure 的情况下,对树的每个操作(例如添加节点)都会返回一棵新树。

    使树创建原子化。您可以创建特殊格式,然后反序列化树。由于所有序列化方法都是内部的,因此您不必公开任何可变方法。

    就在工厂返回构造树之前,先用标志锁定它。这是原子操作的类比。

    使用包级别的方法来构造树。这样外部包就无法访问节点上的变异方法。

    在访问节点时动态创建节点。这意味着您的内部树表示永远不会更改,因为更改节点对您的树结构没有影响。

【讨论】:

【参考方案4】:

我最近遇到了类似的问题 - https://medium.com/hibob-engineering/from-list-to-immutable-hierarchy-tree-with-scala-c9e16a63cb89

这种方法是自下而上构建树,首先构建底部节点,然后向上到顶部节点。

第 1 阶段 - 按深度排序

为了从底部开始,算法按节点在层次结构中的深度对节点进行排序(有一个 O(n) 的方法,您将在链接中看到)。

第 2 阶段 - 从底部构建树

底部节点没有子节点,因此算法构造底部节点。

然后,向上一层,用之前处理过一层的节点构造具有子节点的节点。 算法继续进行,直到到达顶部节点。

【讨论】:

【参考方案5】:

构建高效、不可变的数据结构可能具有挑战性。幸运的是,有些人已经想出了如何实现其中的许多。查看here,了解关于各种不可变数据结构的讨论。

这也是我仍在努力让自己加快速度的一个领域,因此我无法推荐您应该查看的这些结构的确切子集,但可以使用一种数据结构可能非常有用的树是拉链。

【讨论】:

【参考方案6】:

在不强制它成为单链树的情况下,是否有任何巧妙的方法来构造这样一棵树并且仍然让所有节点完全不可变?

保持您的接口和实现解耦,并且不要将树的节点限制为与树本身相同的类。

此问题的一个解决方案是将节点层次结构存储在其他一些不可变表示中,当调用者调用 getChildren()getParent() 时,它会从该不可变表示中懒惰地构造子节点。如果您希望 node.getChildren().get(i).getParent() == node 为真(而不是 .equals(node) - 即同一性而不是相等性),那么您必须缓存节点对象,以便您可以相应地重新传递它们。

【讨论】:

【参考方案7】:

构造不可变树的正确方法是让每个节点的构造函数以自身作为参数调用子节点的构造函数,但条件是子节点的构造函数不能导致对自身的根引用存储在任何地方,也不要将传入的参数用于任何目的,除了初始化构造函数除了接受这种初始化之外没有其他目的的字段。此外,父节点的构造函数应避免使用子节点中会取消引用“父”字段的任何成员。

尽管这种技术似乎违反了不可变对象的构造函数不应将羽翼未丰的对象作为其他例程的参数的规则,但“真正的”规则是不可变对象的构造函数不应允许引用以直接或间接访问尚未达到其最终值的任何字段的方式使用的羽翼未丰的对象。一般来说,如果一个羽翼未丰的对象向外部世界公开了对自身的引用,它将无法控制外部代码可以用它做什么。然而,在调用子节点构造函数的特定情况下,假设子节点的代码符合上述要求,则将没有对父节点的根引用,除了通过父节点本身。因此,不会有任何代码对初出茅庐的节点做任何意外的事情都会得到对它的引用。

【讨论】:

【参考方案8】:

由于您希望它们是不可变的,因此您必须在构造时进行。制作一个同时接受父母和孩子的构造函数,而不是两个单独的构造函数。

【讨论】:

这行不通。您将无法将子代添加到第一组子代中。您将只能构建 3 深的树。

以上是关于如何创建一个完全不可变的树层次结构?建筑鸡和蛋的主要内容,如果未能解决你的问题,请参考以下文章

具有深度嵌套层次结构的不可变 NSDictionary:更改键的值?

在 wicket 中,如何创建未由组件层次结构/布局定义的 RadioGroup?

如何编写第一个语言

Java 中不可变对象 String 真的"完全不可改变"吗?

第一个编译器是如何编写的?

xBIM 基础15 IFC的空间层次结构