什么时候可以接受对父指针的循环引用?


24

这个堆栈溢出问题是关于一个孩子通过指针对其父对象的引用。

最初,评论对于设计是一个可怕的想法至关重要。

我了解这通常不是最好的主意。从一般的经验来看,似乎很公平地说:“不要这样做!”

但是,我想知道在什么样的条件下您将需要执行以下操作。这里的问题和相关的答案/注释甚至建议图不要做这样的事情。


1
您链接的问题在这个问题上似乎很全面。
与莫妮卡(Monica)进行的轻度比赛

4
就了解为什么而言, @LightnessRacesinOrbit“不要这样做”并不是真正有用。
enderland

2
我在那里看到的不仅仅是“不要这样做”。我看到了多位专家争论的利弊。
与莫妮卡(Monica)进行的轻度比赛

1
您可能有一个需要遍历的双向列表,某种圆形缓冲区,也许您代表了游戏中两条相连的道路-如果您需要表示某种圆形,那么这可能是个好主意。
2016年

2
我的实用经验法则是一个问题:“孩子可以没有父母陪伴吗?”。(如果考虑XmlDocuments及其节点,那么没有文档的树上下文就无法存在节点。这是胡说八道)。如果答案是否定的,那么双向链接就可以了:您有两个只能一起存在的对象。如果对象可以独立存在,则删除这两个链接之一。
mikalai

Answers:


43

这里的关键不是两个对象是否具有循环引用,而是这些引用是否指示彼此的所有权

两个对象不能彼此“拥有”:这为初始化和删除顺序造成了棘手的难题。一个必须是可选引用,否则必须指示一个对象将无法管理另一个对象的生存期。

考虑一个双向链接的列表:两个节点来回链接,但是两个节点都不“拥有”另一个(列表同时拥有它们)。这意味着两个节点都不会为另一个节点分配内存,也不会负责另一个节点的身份或生存期管理。

树木具有相似的关系,尽管树中的节点可以分配孩子,而父母确实拥有孩子。从孩子到父母的链接有助于遍历,但同样不能定义所有权。

在大多数OO设计中,将另一个对象作为对象的数据成员进行引用意味着所有权。例如,假设我们有Car和Engine类。两者都不是非常有用的。我们可以说这些对象彼此依赖:它们需要存在另一个对象才能执行有用的工作。但是,另一个“拥有”者呢?在这种情况下,我们可以说汽车拥有引擎,因为汽车是所有汽车零部件所处的“容器”。在OO和现实世界设计中,汽车都是其各个部分的总和,并且所有这些部分在汽车的上下文中连接在一起。引擎可能引用了Car,也可能引用了TorqueConverter,

循环引用可能会产生不良的设计气味,但不一定如此。如果明智地使用并正确记录下来,它们可以使数据结构的使用更加容易。

尝试遍历一棵没有父母和孩子之间双向引用的树。当然,您可以提出一种基于堆栈的方法,该方法既脆弱又复杂,或者可以使用非常简单的基于引用的方法。


16

这种设计需要考虑几个方面:

  • 结构依赖性
  • 所有权关系(即组合与其他类型的关联)
  • 导航需求

类之间的结构依赖性:

如果您打算重用组件类,则应避免不必要的依赖性,并避免使用这种封闭的循环结构。

然而,有时两类在概念上是紧密联系在一起的。在这种情况下,避免依赖不是真正的选择。例如:一棵树及其叶子,或更笼统地说是复合材料及其组件

对象的所有权:

一个对象拥有另一个吗?或另外说明:如果一个物体被破坏,另一个物体也应被破坏吗?

这个问题由Snowman进行了深入探讨,因此在此不再赘述。

对象之间的导航需求:

最后一个问题是导航需求。让我们以我最喜欢的示例为例,该结构是“ 四个帮”复合设计模式

伽玛&al。明确提到可能需要有一个明确的父级引用:“ 保持子组件到父级的引用可以简化遍历和管理复合结构 ”当然,您可以想象系统地进行自顶向下的遍历,但是对于非常大的复合对象,会以指数方式显着降低操作速度。直接参考,甚至圆形也可以大大简化复合材料的操作。

一个例子可以是电子系统的图形模型。复合结构可以代表电子板,电路,元件。要显示和操作模型,您需要在GUI视图中提供一些几何代理。从头到尾进行搜索肯定比从用户选择的GUI元素导航到组件更容易,以找出哪个是父元素,以及相关的兄弟/姐妹元素。

当然,正如Gamma等人指出的那样,您必须确保循环关系的不变性。正如您所提到的SO问题所显示的那样,这可能很棘手。但这是完全可管理且安全的方式。

结论

导航需求不应低估。UML并非毫无道理地在建模符号中明确地对其进行了处理。是的,在完全有效的情况下需要循环引用。

唯一的一点是,有时人们倾向于朝着这样的方向迅速发展。因此,在决定是否要这样做之前,有必要考虑所有涉及的三个方面。


1
恕我直言,这个答案是认真的。OP问:“鉴于已经存在父子关系,何时可以通过循环引用来实现”。因此,结构和“所有权”(在这里提到的意义上)已经很清楚了。这意味着在一侧或另一侧添加引用的唯一标准是“导航需求”和“孩子的独立重用”问题。
布朗博士

@DocBrown-只要有资格,您总是可以悬赏该问题。:-)

1
@ GlenH7:那不会给这个答案更多的选票,只有斯诺曼的答案(我认为这有点遗漏了问题的要点)。
布朗

1
@DocBrown-但是repz,伙计,repz!

...并且如果您添加可见性选项(如受公共-私人保护),则会为您提供9种不同的选择:)
mikalai

8

通常,循环引用是一个非常糟糕的主意,因为它们表示循环依赖性。您可能已经知道为什么循环操作不好,但是出于完整性的考虑,tl; dr版本是,每当类A和B都相互依赖时,就无法理解/修复/优化/等A或B,而没有同时了解/修复/优化/其他类。很快就会导致代码库,在这些代码库中,您必须更改所有内容,才能更改任何内容。

但是, 可能具有循环引用而不会产生有害的循环依赖关系。只要参考在功能上严格可选的,则此方法有效。就是说,我的意思是您可以轻松地将其从类中删除,即使它们的运行速度较慢,它们仍然可以工作。对于此类循环非依赖创建的引用,我知道的主要用例是能够快速遍历基于节点的数据结构,例如链表,树和堆。例如,原则上,您可以在双向链表上执行的任何操作,也可以在单链链表上执行的操作,恰好有一些操作(例如,在列表中向后移动)具有更好的大操作,带双链接版本的O。


6

通常这样做不是一个好主意的原因是因为它违反了 Dependency Inversion Principle。人们在此方面写了很多细节,比我在这篇文章中所能介绍的要详细得多,但是归结为它很难维护,因为耦合是如此紧密。更改一个类几乎总是需要对另一个类进行更改,而如果依赖性仅指向一种方式,则接口一侧的更改将被隔离。如果两个类都指向一个抽象接口,那就更好了。

一个主要的例外是,当您在不同的抽象级别上没有两个不同的类,但是没有同一个类的两个节点时,例如,在树中,双向链接列表中,等等。在这里,它更多的是结构关系,而不是结构关系。抽象关系。在这种情况下,出于算法效率的考虑,循环引用是可以接受的,甚至是受鼓励的。


5

甚至建议图不要做这样的事情。

有时,您只需要以自下而上的方式从不同于树的数据结构访问事物,而树则需要以自上而下的方式访问事物。

例如,四叉树可能会将元素存储在矢量图形软件中。但是,用户的选择存储在矢量元素引用/指针的单独选择列表中。当用户想要删除该选择时,我们必须更新四叉树,并且以自下而上的方式从叶子开始而不是自上而下的方式更新树可能会更有效率。否则,对于每个元素,您都必须从根到叶,然后再次备份。


1
是的,这个例子确实合适。正是我在关于组合导航的论点中试图描述的那种东西:反向指针大大提高了速度。感谢您有趣的表演轶事和非常清晰的图表!+1
Christophe

@克里斯托夫,我也喜欢你的榜样!我不确定我是否在正确回答这个问题,因为也许更多的是关于“循环所有权”,而不仅仅是让我们向上/向后遍历数据结构的后向指针。但是我主要是在回答问题的最后一个“图形”部分。

2

毁灭战士3有一个带有指向父对象的指针的子对象的示例。具体来说,它使用侵入式列表。总而言之,一个侵入式列表就像一个链表,只是每个节点都包含一个指向该列表本身的指针。

好处:

  • 当对象可以同时存在于多个列表中时,列表节点的内存只需要分配和释放一次。

  • 当需要销毁某个对象时,您可以轻松地将其从其所在的所有列表中删除,而无需线性搜索每个列表。

我认为这是一个非常特定的场景,但是如果我理解您的问题,那么这是可接受使用包含指向其父对象的指针的子对象的示例。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.