为什么对象引用另一个引用第一个对象的对象是一个糟糕的设计?
类之间的循环依赖关系不一定有害.实际上,在某些情况下,它们是可取的.例如,如果您的应用程序处理宠物及其所有者,您可能希望Pet类具有获取宠物所有者的方法,并且Owner类具有返回宠物列表的方法.当然,这可能会使内存管理变得更加困难(使用非GC语言).但如果循环是问题所固有的,那么试图摆脱它可能会导致更多的问题.
另一方面,模块之间的循环依赖性是有害的.它通常表示思维模糊的结构,和/或未能坚持原始的模块化.通常,具有不受控制的交叉依赖性的代码库将比具有干净的分层模块结构的代码库更难理解并且难以维护.没有像样的模块,预测变化的影响可能要困难得多.这使得维护更加困难,并导致由于构思错误而导致的"代码衰减".
(另外,像Maven这样的构建工具不会处理具有循环依赖关系的模块(artefacts).)
循环引用并不总是有害的 - 在某些用例中它们非常有用.我想到了双重链接列表,图形模型和计算机语言语法.但是,作为一般惯例,您可能希望避免对象之间的循环引用有几个原因.
数据和图表一致性.使用循环引用更新对象可能会产生挑战,确保在所有时间点对象之间的关系有效.这种类型的问题经常出现在对象关系建模实现中,其中在实体之间找到双向循环引用并不罕见.
确保原子操作.确保循环引用中两个对象的更改都是原子的可能会变得复杂 - 尤其是涉及多线程时.确保可从多个线程访问的对象图的一致性需要特殊的同步结构和锁定操作,以确保没有线程看到一组不完整的更改.
物理隔离挑战.如果两个不同的类A和B以循环方式相互引用,则将这些类分成独立的程序集会变得很有挑战性.当然可以创建具有A和B实现的接口IA和IB的第三个组件; 允许每个人通过这些接口引用另一个.也可以使用弱类型引用(例如对象)作为打破循环依赖的方法,但是然后无法轻易访问对这种对象的方法和属性的访问 - 这可能会破坏具有引用的目的.
执行不可变循环引用.像C#和VB这样的语言提供了关键字,允许对象内的引用是不可变的(只读).不可变引用允许程序确保引用在对象的生命周期中引用相同的对象.不幸的是,使用编译器强制的不变性机制来确保循环引用不能更改并不容易.它只能在一个对象实例化另一个对象时才能完成(参见下面的C#示例).
class A { private readonly B m_B; public A( B other ) { m_B = other; } } class B { private readonly A m_A; public A() { m_A = new A( this ); } }
程序可读性和可维护性.循环引用本身就很脆弱且易于破解.这部分源于这样一个事实,即阅读和理解包含循环引用的代码比避免它们的代码更难.确保您的代码易于理解和维护有助于避免错误并允许更轻松,更安全地进行更改.具有循环引用的对象更难以进行单元测试,因为它们不能彼此隔离地进行测试.
对象生存期管理.虽然.NET的垃圾收集器能够识别和处理循环引用(并正确处理这些对象),但并非所有语言/环境都可以.在对其垃圾收集方案使用引用计数的环境中(例如VB6,Objective-C,某些C++库),循环引用可能导致内存泄漏.由于每个对象都保留在另一个对象上,因此它们的引用计数永远不会达到零,因此永远不会成为收集和清理的候选对象.
因为现在他们真的是一个单一的对象.你不能孤立地测试任何一个.
如果您修改一个,那么您可能也会影响其伴侣.
来自维基百科:
循环依赖可能会在软件程序中引起许多不良影响.从软件设计的观点来看,最棘手的问题是相互依赖的模块的紧密耦合,这减少或使得单个模块的单独重复使用变得不可能.
当一个模块中的一个小的局部变化扩散到其他模块并且具有不希望的全局影响(程序错误,编译错误)时,循环依赖会导致多米诺骨牌效应.循环依赖性也可能导致无限递归或其他意外故障.
循环依赖还可能通过阻止某些非常原始的自动垃圾收集器(那些使用引用计数)来解除分配未使用的对象,从而导致内存泄漏.
这样的对象可能很难被创建和销毁,因为为了非原子地执行,你必须违反引用完整性来首先创建/销毁一个,然后另一个(例如,你的SQL数据库可能会对此不屑一顾).它可能会混淆你的垃圾收集器.Perl 5,它使用简单的引用计数进行垃圾收集,不能(没有帮助)因此它的内存泄漏.如果这两个对象现在属于不同的类,则它们是紧密耦合的,不能分开.如果您有一个包管理器来安装这些类,则循环依赖关系会扩展到它.它必须知道要安装两个包之前测试它们,(说作为构建系统的维护者)是一个PITA.
也就是说,这些都可以克服,并且通常需要具有循环数据.现实世界不是由整齐的有向图组成.许多图表,树木,地狱,双链表是循环的.