Warning: file_get_contents(/data/phpspider/zhask/data//catemap/4/oop/2.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
.net 为什么循环引用被认为是有害的?_.net_Oop - Fatal编程技术网

.net 为什么循环引用被认为是有害的?

.net 为什么循环引用被认为是有害的?,.net,oop,.net,Oop,为什么一个对象引用另一个对象而引用第一个对象是错误的设计?因为现在它们实际上是一个单独的对象。你不能单独测试任何一个 如果你修改了一个,很可能也会影响到它的同伴。来自维基百科: 循环依赖可能会导致许多问题 软件程序中的有害影响。 最有问题的是软件 设计的角度是紧的 相互依赖系统的耦合 减少或制造的模块 不可能单独重复使用 单模块 循环依赖关系可能会导致 多米诺效应当一个小的局部 一个模块的变化扩散到另一个模块 其他模块和具有不需要的全局 影响(程序错误、编译错误) 错误)。循环依赖可以 也会导致

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

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

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

来自维基百科:

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

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

循环依赖也可能导致 通过防止某些 非常原始的自动垃圾 收集器(使用引用的收集器) 计数)从释放未使用的 对象


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

这样的对象很难创建和销毁,因为要以非原子方式创建或销毁对象,必须先创建/销毁一个对象,然后再创建/销毁另一个对象(例如,您的SQL数据库可能对此犹豫不决)。这可能会混淆垃圾收集器。Perl 5使用简单的引用计数进行垃圾收集,但它不能(没有帮助)因此它是内存泄漏。如果这两个对象现在属于不同的类,那么它们是紧密耦合的,不能分离。如果您有一个包管理器来安装这些类,那么循环依赖关系就会扩展到它。它必须知道在测试它们之前安装这两个包,这(作为构建系统的维护者)是一个PITA


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

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

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

另一方面,模块之间的循环依赖是有害的。这通常表明考虑不周的模块结构,和/或未能坚持最初的模块化。通常,具有不受控制的交叉依赖关系的代码库比具有干净的分层模块结构的代码库更难理解和维护。如果没有像样的模块,就很难预测变化的影响。这使得维护变得更加困难,并导致由于考虑不周的修补而导致“代码衰退”


(同样,像Maven这样的构建工具不会处理具有循环依赖关系的模块(人工制品)。

这里有几个例子可以帮助说明为什么循环依赖关系不好

问题#1:首先初始化/构造什么

考虑以下示例:

class A
{
  public A()
  {
    myB.DoSomething();
  }

  private B myB = new B();
}

class B
{
  public B()
  {
    myA.DoSomething();
  }

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

问题2:

在此情况下,我已经更改为非托管C++示例,因为通过设计实现.NET,将问题隐藏起来。但是,在下面的示例中,问题将变得非常清楚。我很清楚.NET并没有真正将引用计数用于内存管理。我在这里使用它只是为了说明核心问题。还要注意,我在这里演示了问题1的一种可能解决方案

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

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

循环引用并不总是有害的——在某些用例中它们可能非常有用。我想到了双链表、图形模型和计算机语言语法。然而,作为一种常规做法,您可能希望避免循环引用的原因有很多
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
...
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 ); }
}