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

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

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

评论

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

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

#1 楼

循环引用有很多错误:


循环类引用产生高耦合;每次都更改它们时,两个类都必须重新编译。
循环汇编引用会阻止静态链接,因为B依赖于A,但是A在B完成之前才能被汇编。具有堆栈溢出的算法(例如序列化程序,访问者和漂亮打印机)。更高级的算法将具有循环检测功能,并且只会失败,并带有描述性更强的异常/错误消息。
循环对象引用也使依赖注入成为不可能,从而大大降低了系统的可测试性。大量的循环引用通常是上帝的对象。即使不是这样,它们也倾向于生成Spaghetti代码。
循环实体引用(尤其是在数据库中,但在域模型中)也会阻止使用不可为空的约束,这可能最终导致数据损坏
一般来说,圆形引用只是在试图理解程序的功能时容易造成混乱,并极大地增加了认知负担。

请考虑一下孩子;尽可能避免使用循环引用。

评论


我特别感谢最后一点,“认知负荷”是我非常意识到的东西,但是从来没有一个非常简洁的术语。

–dash-tom-bang
2010-10-14在16:40

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

–kevin cline
2012年10月29日18:35

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

– BlueRaja-Danny Pflughoeft
2014年4月29日在15:24

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

– Aaronaught
2014年4月30日在1:53

我并不是说这是一个好习惯,而是指出答案中所说的并非不可能。

– BlueRaja-Danny Pflughoeft
2014年4月30日在3:09



#2 楼

循环引用是非循环引用的两倍。

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

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

#3 楼

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

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

#4 楼

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

#5 楼

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

考虑一个XML DOM-每个节点都具有意义对父母的引用,并为每个父母提供其子女的清单。从逻辑上讲,该结构是一棵树,但是从垃圾回收算法或类似的角度来看,该结构是圆形的。

评论


那不是一棵树吗?

–康拉德·弗里克斯(Conrad Frix)
2010-10-14 1:31

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

–比利·奥尼尔(Billy ONeal)
2010-10-14 2:14

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

–康拉德·弗里克斯(Conrad Frix)
10-10-14在15:21

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

–马特·奥莱尼克(Matt Olenik)
2010-10-14 17:04

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

–byxor
16-11-5在14:00



#6 楼

就像鸡肉或鸡蛋问题一样。

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

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

#7 楼

尽管我同意这里的大多数评论,但我还是要为“父母” /“孩子”循环引用提出特殊情况。

一个类经常需要了解有关其父类或拥有类的知识(可能是默认行为),数据来自的文件名,选择该列的sql语句或日志文件的位置等。

您可以在没有循环引用的情况下执行此操作具有一个包含类,这样以前的“父级”现在可以成为同级,但是并非总是可以重构现有代码来做到这一点。

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

评论


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

–超级猫
18-10-15在21:53

#8 楼

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

#9 楼

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

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

特别是使用循环引用的地方是,我还没有看到这样的情况:除了一个自我参考之外,实际上将需要它们。如果您要对树或图进行建模,就完全可以了,因为从代码质量的角度来看,自引用是无害的(不添加依赖项)。

我相信目前您开始需要非自我参考,立即应该询问是否无法将其建模为图形(将多个实体折叠为一个节点)。也许在某些情况下,您需要进行循环引用,但是将其建模为图形是不合适的,但是我对此表示高度怀疑。

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

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

因此,除非绝对明确它来自我自己,否则我不允许循环引用发生建模。

(注意:这在数据库设计上有些偏颇,但我敢打赌它也同样适用于其他领域)

#10 楼

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

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

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

评论


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

–疫苗
2010-10-14 14:11

#11 楼

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

#12 楼

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

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

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

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

#13 楼

一些垃圾收集器在清理它们时遇到了麻烦,因为每个对象都被另一个对象引用。

编辑:正如下面的注释所指出的,这仅适用于极幼稚的垃圾收集器尝试,而并非如此在实践中会遇到的一种。

评论


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

–比利·奥尼尔(Billy ONeal)
2010-10-14在0:37

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

–迪恩·哈丁(Dean Harding)
2010-10-14 0:40

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

–肯·布鲁姆
2010-10-14 13:45

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

–克里斯
2010-10-14 13:46

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

–DexterW
2010-10-14 14:06

#14 楼

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

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

真正的问题来了在运行时数据结构的循环性方面,实际上无法以消除循环性的方式定义一些问题。最终,这是应该决定的问题,需要其他任何条件迫使程序员解决不必要的难题。

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

评论


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

–user40980
2014年5月11日在2:11

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

–乔什(Josh S)
2014年5月11日6:37



我发现它可以使您的程序更容易启动和运行,但是总的来说,由于您发现微不足道的更改具有级联作用,因此最终使维护软件变得更加困难。 A向B发出呼叫,这又向A发出呼叫,这又向B回叫...我发现很难真正了解这种性质的变化的影响,尤其是当A和B是多态的时。

–dash-tom-bang
2014年5月14日20:06