Here are a couple of examples that may help illustrate why circular dependencies are bad.
Problem #1: What gets initialized/constructed first?
Consider the following example:
class A
{
public A()
{
myB.DoSomething();
}
private B myB = new B();
}
class B
{
public B()
{
myA.DoSomething();
}
private A myA = new A();
}
Which constructor is called first? There's really no way to be sure because it's completely ambiguous. One or the other of the DoSomething methods is going to be called on an object that's uninitialized., resulting in incorrect behavior and very likely an exception being raised. There are ways around this problem, but they're all ugly and they all require non-constructor initializers.
Problem #2:
In this case, I've changed to a non-managed C++ example because the implementation of .NET, by design, hides the problem away from you. However, in the following example the problem will become pretty clear. I'm well aware that .NET doesn't really use reference counting under the hood for memory management. I'm using it here solely to illustrate the core problem. Note also that I've demonstrated here one possible solution to problem #1.
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
...
At first glance, one might think that this code is correct. The reference counting code is pretty simple and straightfoward. However, this code results in a memory leak. When A is constructed, it initially has a reference count of "1". However, the encapsulated myB variable increments the reference count, giving it a count of "2". When localA is released, the count is decremented, but only back to "1". Hence, the object is left hanging and never deleted.
As I mentioned above, .NET doesn't really use reference counting for its garbage collection. But it does use similar methods to determine if an object is still being used or if it's OK to delete it, and almost all such methods can get confused by circular references. The .NET garbage collector claims to be able to handle this, but I'm not sure I trust it because this is a very thorny problem. Go, on the other hand, gets around the problem by simply not allowing circular references at all. Ten years ago I would have preferred the .NET approach for its flexibility. These days, I find myself preferring the Go approach for its simplicity.