代码之家  ›  专栏  ›  技术社区  ›  Pierreten

为什么循环引用被认为是有害的[关闭]

  •  71
  • Pierreten  · 技术社区  · 16 年前

    为什么一个对象引用另一个对象而引用第一个对象是一个糟糕的设计?

    11 回复  |  直到 7 年前
        1
  •  70
  •   jaco0646    10 年前

    之间的循环依赖关系 不一定有害。事实上,在某些情况下,它们是可取的。例如,如果您的应用程序处理宠物及其所有者,则您希望Pet类有一个方法来获取宠物的所有者,而所有者类有一个方法返回宠物列表。当然,这会使内存管理更加困难(在非GC语言中)。但是,如果循环性是问题固有的,那么试图消除它可能会导致更多的问题。

    另一方面,它们之间存在循环依赖关系 模块

        2
  •  58
  •   Shawn Chin    13 年前

    循环引用并不总是有害的 -在一些用例中,它们可能非常有用。我想到了双链表、图形模型和计算机语言语法。但是,作为一般实践,有几个原因可以解释为什么要避免对象之间的循环引用。

    1. 数据和图形的一致性。 使用循环引用更新对象可能会在确保对象之间的关系在所有时间点都有效方面带来挑战。这类问题通常出现在对象关系建模实现中,在实体之间找到双向循环引用并不少见。

    2. 确保循环引用中两个对象的更改都是原子性的可能会变得复杂,特别是当涉及多线程时。确保可从多个线程访问的对象图的一致性需要特殊的同步结构和锁定操作,以确保没有线程看到不完整的更改集。

    3. 物理分离挑战。 如果两个不同的类A和B以循环方式相互引用,则将这些类分离为独立的程序集可能会变得很困难。当然可以创建第三个程序集,其中包含a和B实现的IA和IB接口;允许每个接口通过这些接口相互引用。也可以使用弱类型引用(例如object)来打破循环依赖关系,但这样就无法轻松访问此类对象的方法和属性-这可能会破坏拥有引用的目的。

    4. 强制执行不可变的循环引用。 像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 ); }
      }
      
    5. 循环引用本质上是脆弱的,容易被破坏。这部分是因为阅读和理解包含循环引用的代码比避免循环引用的代码更难。确保您的代码易于理解和维护有助于避免bug,并允许更轻松、更安全地进行更改。具有循环引用的对象更难进行单元测试,因为它们不能彼此隔离地进行测试。

    6. 对象生命周期管理。 尽管.NET的垃圾收集器能够识别和处理循环引用(并正确处理此类对象),但并非所有语言/环境都能。在使用引用计数的垃圾收集方案(例如VB6、Objtovi-C、一些C++库)的环境中,循环引用可能导致内存泄漏。由于每个对象都会抓住另一个对象,因此它们的引用计数永远不会达到零,因此永远不会成为收集和清理的候选对象。

        3
  •  8
  •   duffymo    16 年前

    因为现在它们实际上是一个单一的物体。你不能单独测试任何一个。

    如果你修改了一个,很可能你也会影响到它的同伴。

        4
  •  7
  •   Paul    16 年前

    来自维基百科:

    循环依赖可能会导致许多问题 软件程序中的有害影响。 设计的角度是紧的 减少或制造的模块 不可能单独重复使用 单模块。

    循环依赖关系可能会导致 多米诺效应当一个小的局部 一个模块的变化扩散到另一个模块 其他模块和具有不需要的全局 错误)。循环依赖可以 也会导致无限递归或 其他意外故障。

    循环依赖也可能导致 收集器(使用引用的收集器) 计数)从释放未使用的 物体。

        5
  •  5
  •   Schwern    16 年前

    二者都 包装

    也就是说,这些都是可以克服的,通常需要循环数据。现实世界不是由整洁的有向图组成的。很多图,树,地狱,一个双链表是循环的。

        6
  •  2
  •   Konamiman    16 年前

    这会损害代码的可读性。从循环依赖到意大利面代码,这只是一小步。

        7
  •  2
  •   Russell Newquist    16 年前

    考虑下面的例子:

    class A
    {
      public A()
      {
        myB.DoSomething();
      }
    
      private B myB = new B();
    }
    
    class B
    {
      public B()
      {
        myA.DoSomething();
      }
    
      private A myA = new A();
    }
    

    首先调用哪个构造函数?真的没有办法确定,因为它完全是模棱两可的。将对未初始化的对象调用一个或另一个DoSomething方法,从而导致错误行为,很可能引发异常。有很多方法可以解决这个问题,但它们都很难看,而且都需要非构造函数的初始值设定项。

    问题2:

    class B;
    
    class A
    {
    public:
      A() : Refs( 1 )
      {
        myB = new B(this);
      };
    
      ~A()
      {
        myB->Release();
      }
    
      int AddRef()
      {
        return ++Refs;
      }
    
      int Release()
      {
        --Refs;
        if( Refs == 0 )
          delete(this);
        return Refs;
      }
    
      B *myB;
      int Refs;
    };
    
    class B
    {
    public:
      B( A *a ) : Refs( 1 )
      {
        myA = a;
        a->AddRef();
      }
    
      ~B()
      {
        myB->Release();
      }
    
      int AddRef()
      {
        return ++Refs;
      }
    
      int Release()
      {
        --Refs;
        if( Refs == 0 )
          delete(this);
        return Refs;
      }
    
      A *myA;
      int Refs;
    };
    
    // Somewhere else in the code...
    ...
    A *localA = new A();
    ...
    localA->Release(); // OK, we're done with it
    ...
    

    乍一看,人们可能会认为这段代码是正确的。参考计数代码非常简单和直接。但是,此代码会导致内存泄漏。构造时,它最初的引用计数为“1”。但是,封装的myB变量会增加引用计数,使其计数为“2”。释放localA时,计数将递减,但仅返回到“1”。因此,该对象保持挂起状态,并且从不删除。

    正如我前面提到的,.NET并没有真正使用引用计数进行垃圾收集。但它确实使用类似的方法来确定对象是否仍在使用,或者是否可以删除它,并且几乎所有这些方法都会被循环引用弄糊涂。NET垃圾收集器声称能够处理这个问题,但我不确定我是否信任它,因为这是一个非常棘手的问题。另一方面,Go通过根本不允许循环引用来绕过这个问题。十年前,我更喜欢.NET方法的灵活性。这些天来,我发现自己更喜欢Go方法,因为它简单。

        8
  •  1
  •   Alex    16 年前

        9
  •  1
  •   leon    12 年前

    参考LakOSS书,在C++软件设计中,循环物理依赖是不可取的。有几个原因

    • 这使得它们很难测试,也不可能独立重用。
    • 这使得人们很难理解和维护它们。
        10
  •  1
  •   Sumit Sengar    11 年前

    循环引用似乎是一种合法的领域建模场景。例如Hibernate和许多其他ORM工具鼓励实体之间的这种交叉关联,以实现双向导航。在在线拍卖系统中的典型示例中,卖家实体可以维护对他/她正在出售的实体列表的引用。而且每个项目都可以维护对其相应卖方的引用。

        11
  •  -2
  •   Muhammad Soliman    16 年前

    NET垃圾收集器可以处理循环引用,因此在.NET framework上工作的应用程序不必担心内存泄漏。