循环引用有什么问题?


160

我今天参加了一次编程讨论,在那儿我做了一些声明,这些声明基本上是不合逻辑地假定循环引用(在模块,类之间,无论如何)通常是不好的。一经讲完,我的同事就会问:“循环引用有什么问题?”

我对此有很强烈的感觉,但我很难简洁而具体地表达自己的观点。我可能提出的任何解释都倾向于依赖于我也考虑过公理的其他项目(“不能孤立使用,因此无法测试”,“参与对象中状态发生变化时的未知/不确定行为”,等等。 。),但是我很想听听一个简洁的原因,为什么循环引用很糟糕,却没有我自己的大脑所经历的那种信念飞跃,多年来花了很多时间来弄清它们的理解,修正,并扩展各种代码。

编辑:我不是在问同质的循环引用,就像那些在双链表或指向父母的指针。这个问题的确是在询问“更大范围”的循环引用,例如libA调用libB,后者又回调libA。如果愿意,可将“模块”替换为“库”。到目前为止,感谢您提供所有答案!


循环引用是否与库和头文件有关?在工作流程中,新的ProjectB代码将处理从旧版ProjectA代码输出的文件。ProjectA的输出是由ProjectB驱动的新要求;ProjectB的代码可以方便地通用地确定哪些字段在何处等。重点是,遗留ProjectA可以在新的ProjectB中重用代码,而ProjectB愚蠢的是不在遗留的ProjectA中重用实用程序代码(例如,字符集检测和代码转换,记录解析,数据验证和转换等)。
Luv2code 2015年

1
@ Luv2code仅当您在项目之间剪切和粘贴代码或者两个项目都在同一代码中进行编译和链接时,它才会变得愚蠢。如果他们共享这样的资源,请将它们放入库中。
dash-tom-bang '02

Answers:


220

循环引用有很多错误:

  • 循环引用创建高耦合 ; 这两个类必须重新编译每次无论是他们的改变。

  • 循环装配引用可防止静态链接,因为B依赖于A,但是直到B完成才可以装配A。

  • 循环对象引用会使堆栈溢出而使朴素的递归算法(例如序列化程序,访问者和漂亮的打印机)崩溃。更高级的算法将具有周期检测功能,并且只会失败,并带有更具描述性的异常/错误消息。

  • 循环对象引用还使依赖项注入成为不可能,从而大大降低了系统的可测试性

  • 具有大量循环引用的对象通常是“ 上帝对象”。即使不是,它们也倾向于导致制定《意大利面条法》

  • 循环实体引用(尤其是在数据库中,但在域模型中也是如此)会阻止使用不可为空的约束,这可能最终导致数据损坏或至少不一致。

  • 当试图理解程序的功能时,一般来说,循环引用只会造成混乱,并极大地增加认知负担。

请想想孩子们;尽可能避免使用循环引用。


32
我特别感谢最后一点,“认知负荷”是我非常了解的东西,但是从来没有一个非常简洁的术语。
dash-tom-bang 2010年

6
好答案。如果您说一些有关测试的信息,那就更好了。如果模块A和B相互依赖,则必须一起测试。这意味着它们并不是真正独立的模块。它们在一起是一个损坏的模块。
凯文·克莱恩

5
对于循环引用,即使使用自动DI,也不是不可能进行依赖注入。只需注入一个属性而不是构造函数参数即可。
BlueRaja-Danny Pflughoeft 2014年

3
@ BlueRaja-DannyPflughoeft:我认为像其他许多DI的从业者一样,这是一种反模式,因为(a)不清楚属性实际上是否是一个依赖项,并且(b)被“注入”的对象不容易跟踪自己的不变量。更糟糕的是,如果无法解决依赖关系,许多最复杂/受欢迎的框架(如Castle Windsor)将无法给出有用的错误消息。您最终会得到一个烦人的null引用,而不是详细地解释了无法解析构造函数的依赖关系。仅仅因为你可以,并不意味着你应该
2014年

3
我并不是说这是一个好习惯,我只是指出答案中所说的并非不可能。
BlueRaja-Danny Pflughoeft 2014年

22

圆形参考是非圆形参考耦合的两倍。

如果Foo知道Bar,而Bar知道Foo,则您有两件事需要更改(当要求Foos和Bars不再必须彼此了解时)。如果Foo知道Bar,但是Bar不知道Foo,则可以在不触摸Bar的情况下更改Foo。

循环引用还可能导致引导问题,至少在长时间持续的环境(部署的服务,基于图像的开发环境)中,Foo依赖Bar进行工作以进行加载,但是Bar也依赖Foo进行工作以进行引导。加载。


17

当将两段代码绑定在一起时,实际上就是一大段代码。维护少量代码的困难至少在于其大小的平方,甚至可能更高。

人们经常查看单个类(/函数/文件/等)的复杂性,却忘记了您确实应该考虑最小的可分离(可封装)单元的复杂性。具有循环依赖性可能会在无形中增加该单元的大小(直到您开始尝试更改文件1并意识到这还需要更改文件2-127)。


14

它们可能不是一个人就坏的情况,而是表明可能的不良设计的指标。如果Foo依赖于Bar,而Bar依赖于Foo,则有理由质疑为什么它们是两个而不是唯一的FooBar。


10

嗯...这取决于您所说的循环依赖,因为实际上有些循环依赖我认为是非常有益的。

考虑一个XML DOM-每个节点都有对其父级的引用,每个父级都有其子级列表是有意义的。该结构在逻辑上是一棵树,但是从垃圾回收算法或类似的角度来看,该结构是圆形的。


1
那不是一棵树吗?
康拉德·弗里克斯

@康拉德:我想它可以被认为是一棵树,是的。为什么?
Billy ONeal 2010年

1
我不认为树是圆形的,因为您可以向下导航树的子级并将其终止(无论父级引用如何)。除非一个节点有一个孩子,但这个孩子也是祖先,但在我看来,它使它成为图而不是树。
康拉德·弗里克斯

5
循环引用将是节点的子级之一环回祖先。
Matt Olenik

这并不是真正的循环依赖(至少不会以引起任何问题的方式)。例如,假设这Node是一个类,该类本身具有Node对孩子的其他引用。因为只引用自身,所以该类是完全独立的,并且不与其他任何对象耦合。---使用此参数,您可能会认为递归函数是循环依赖项。这(虽然短暂的),但并不坏。
byxor

9

就像鸡肉或鸡蛋的问题。

在许多情况下,循环引用是不可避免的并且很有用,但是例如,在以下情况下,循环引用不起作用:

项目A取决于项目B,而项目B取决于A。需要将A编译为要在B中使用,这需要B在A之前进行编译,而B则需要B在A之前进行编译,而B在A之前...


6

尽管我同意这里的大多数评论,但我想对“父母” /“孩子”通函进行特殊说明。

一个类通常需要了解有关其父类或所属类的信息,也许是默认行为,数据来自的文件名,选择该列的sql语句或日志文件的位置等。

您可以通过具有一个包含类来实现此操作而无需循环引用,从而使以前的“父级”现在成为同级,但并非总是可以重构现有代码来做到这一点。

另一种选择是将子代可能需要的所有数据传递到其构造函数中,这最终简直太可怕了。


与此相关的是,X可能引用Y的原因有两个常见原因:X可能想让Y代表X做事,或者Y可能期望X代表Y为Y做事。如果仅存在Y的引用是出于其他对象希望代表Y进行操作的目的,则应告知此类引用的持有人不再需要Y的服务,并且应在以下位置放弃对Y的引用他们的方便。
超级猫

5

用数据库术语来说,具有正确的PK / FK关系的循环引用使得不可能插入或删除数据。如果您不能从表a中删除,除非该记录从表b中删除,并且您不能从表b中删除,除非该记录是从表a中删除,则无法删除。与插入相同。这就是为什么如果有循环引用,许多数据库不允许您设置级联更新或删除的原因,因为在某些时候,这是不可能的。是的,您可以在不正式声明PK / Fk的情况下建立此类关系,但是(我的经验中有100%的时间)您将遇到数据完整性问题。那只是不好的设计。


4

我将从建模的角度考虑这个问题。

只要您不添加实际上不存在的任何关系,就可以保证安全。如果确实添加它们,则数据的完整性会降低(因为存在冗余),并且代码耦合度会更高。

具体来说,使用循环引用时,除了一个-自引用,我还没有看到实际上需要它们的情况。如果您对树或图形进行建模,则需要这样做,因为从代码质量的角度来看,自引用是无害的(没有添加依赖项),因此完全可以。

我相信,当您开始需要非自我参考时,应立即询问是否不能将其建模为图形(将多个实体折叠成一个节点)。也许您之间有一个循环参考,但是将其建模为图形是不合适的,但我对此表示高度怀疑。

人们认为他们需要一个循环参考,但实际上并不需要。最常见的情况是“一对多情况”。例如,您有一个具有多个地址的客户,应将其中的一个地址标记为主要地址。将这种情况建模为两个独立的关系has_addressis_primary_address_of很诱人,但这是不正确的。原因是作为主要地址不是用户和地址之间的单独关系,而是它是具有地址的关系的属性。这是为什么?因为其域仅限于用户的地址,而不是那里的所有地址。您选择一个链接并将其标记为最强(主要)。

(现在要谈论数据库)许多人选择双向关系解决方案,因为他们理解“主”是唯一的指针,而外键则是一种指针。因此,外键应该是要使用的东西,对吗?错误。外键表示关系,但“主”不是关系。这是排序的退化情况,其中一个元素高于所有元素,其余元素不排序。如果您需要对总订购进行建模,那么您当然会将其视为关系的属性,因为基本上没有其他选择。但是,当您退化它时,有一种选择和一个非常可怕的选择-将非关系模型化为关系模型。因此,关系冗余无疑是不容小under的。

因此,除非绝对清楚它来自我正在建模的事物,否则我不允许循环引用。

(注意:这与数据库设计略有偏差,但是我敢打赌它也同样适用于其他领域)


2

我会用另一个问题回答这个问题:

在保持循环参考模型是您要构建的最佳模型的情况下,您能给我什么情况?

根据我的经验,最好的模型几乎不会像我认为的那样涉及循环引用。话虽如此,在很多模型中,您一直都在使用循环引用,这是非常基础的。父级->子级关系,任何图形模型等,但是这些都是众所周知的模型,我认为您是完全在指其他事物。


1
对于一个应该“永不停止”的程序(将重要的N项粘贴到队列中,并在其中添加一个),循环链接列表(单链接或双链接)可能是中央事件队列的出色数据结构。设置“不删除”标志,然后简单地遍历队列直到空;当需要新任务(瞬态或永久性)时,将它们粘贴在队列上的适当位置;每当您提供带有“不删除”标志的偶数时,然后将其从队列中移除)。
Vatine 2010年

1

数据结构中的循环引用有时是表达数据模型的自然方法。在编码方面,它绝对不是理想的,并且可以(在某种程度上)通过依赖注入来解决,从而将问题从代码推向数据。


1

循环引用构造不仅从设计的角度来看还是有问题的,而且从捕获错误的角度来看也是有问题的。

考虑代码失败的可能性。您尚未在任何一个类中都放置适当的错误捕获,或者是因为您尚未开发方法,或者是您很懒。无论哪种方式,您都不会收到错误消息来告诉您发生了什么,因此您需要对其进行调试。作为一名优秀的程序设计人员,您知道哪些方法与哪些流程相关,因此可以将其范围缩小到与导致错误的流程相关的那些方法。

使用循环引用,您的问题现在增加了一倍。因为您的进程是紧密绑定的,所以您无法知道哪个类可能导致错误,或者错误从何而来导致错误,因为一个类依赖于另一个而另一个依赖。现在,您必须花时间一起测试两个类,以找出哪个类真正导致了错误。

当然,正确的错误捕获可以解决此问题,但前提是您知道何时可能发生错误。而且,如果您使用的是通用错误消息,您的状况仍然不会更好。


1

一些垃圾收集器很难清理它们,因为每个对象都被另一个对象引用。

编辑:正如下面的评论所指出的,这仅适用于对垃圾收集器的幼稚尝试,而不是您在实践中会遇到的尝试。


11
嗯..任何被此绊倒的垃圾收集器都不是真正的垃圾收集器。
Billy ONeal 2010年

11
我不知道有任何现代垃圾收集器会在循环引用方面出现问题。如果您使用引用计数,则循环引用是一个问题,但是大多数垃圾收集器都在跟踪样式(从已知引用列表开始,然后跟随它们查找所有其他引用,收集所有其他内容)。
Dean Harding

4
请参阅sct.ethz.ch/teaching/ws2005/semspecver/slides/takano.pdf,他解释了各种类型的垃圾收集器的缺点-如果进行标记和清除并开始对其进行优化以减少较长的暂停时间(例如创建世代) ,圆形结构开始出现问题(圆形对象的世代不同)。如果进行参考计数并开始解决循环参考问题,最终将引入较长的暂停时间,这是标记和扫描的特征。
肯·布鲁姆

如果垃圾收集器查看了Foo并重新分配了其内存(在此示例中引用Bar),则它应处理Bar的移除。因此,此时不需要垃圾收集器继续删除bar,因为它已经这样做了。反之亦然,如果它删除引用了Foo的Bar,它也应该也删除Foo,因此它不需要去删除Foo,因为它在删除Bar时也这样做了吗?如果我错了,请纠正我。
克里斯,2010年

1
在Objective-C中,使用循环引用可以使释放时引用计数不会为零,这会使垃圾收集器崩溃。
DexterW

-2

我认为不受限制的引用使程序设计更容易,但是我们都知道某些编程语言在某些情况下缺乏对它们的支持。

您提到了模块或类之间的引用。在这种情况下,这是程序员预先定义的静态内容,并且程序员很可能可以搜索缺乏圆度的结构,尽管它可能无法完全解决问题。

真正的问题在于运行时数据结构中的循环性,其中某些问题实际上无法通过消除循环性的方式来定义。但最终-应该指出的问题是,要求其他条件迫使程序员解决不必要的难题。

我会说这是工具的问题而不是原理的问题。


添加一个句子的意见不会显着有助于帖子或解释答案。您能详细说明一下吗?

好两点,发布者实际上提到了模块或类之间的引用。在这种情况下,这是程序员预先定义的静态内容,并且程序员很可能可以搜索缺乏圆度的结构,尽管它可能无法完全解决问题。真正的问题在于运行时数据结构中的循环性,其中某些问题实际上无法通过消除循环性的方式来定义。但最终-应该指出的问题是,要求其他条件迫使程序员解决不必要的难题。
2014年

我发现,它可以使您的程序更容易启动和运行,但是总的来说,由于您发现琐碎的更改具有级联作用,因此最终使维护软件变得更加困难。A向B发出呼叫,这又向A发出呼叫,这又向B回叫...我发现很难真正理解这种性质的变化的影响,尤其是当A和B是多态的时。
dash-tom-bang
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.