同一个程序集中的循环引用是件坏事吗?

时间:2012-03-07 15:23:15

标签: c# .net circular-dependency circular-reference

假设我在同一个程序集中有以下类

public class ParentClass : IDisposable
{
  public ChildClass Child
  {
    get { return _child; }
  }
}   

public class ChildClass 
{
   public ParentClass Parent
   {
     get { return _parent; }
     set { _parent= value; }
   }

   public ChildClass (ParentClass parent)
   {
     Parent= parent;
   }

}

如果我错了,请纠正我,但这是糟糕的设计。这会导致内存泄漏或其他一些不可预见的问题吗?显然垃圾收集器能够处理such kind of circular references

修改

如果这两个类最终在其他类中被这样使用怎么办?

ParentClass objP = new ParentClass ();
ChildClass objC =new ChildClass(objP);
objP.Child = objC;

请注意......

4 个答案:

答案 0 :(得分:18)

不要担心垃圾收集器;它可以轻松处理具有任意拓扑的参考图。担心编写可以通过简化违反不变量来创建错误的对象。

这是一个值得怀疑的设计,不是因为它强调GC - 它没有 - 而是因为它不强制所需的语义不变量:如果X是Y的父,那么Y必须是X的孩子。

编写保持一致的父子关系的类可能非常棘手。我们在Roslyn团队中所做的是实际构建两个树。 “真正的”树只有子引用;没有孩子知道它的父母。我们在其上层叠了一个“facade”树,它强制了父子关系的一致性:当你向父节点询问它的子节点时,它会在它的真实子节点之上创建一个外观并设置该外观对象的父节点。成为真正的父母。

更新:评论员布莱恩要求提供更多细节。下面是如何在仅包含子引用的“绿色”树上实现带有子引用和父引用的“红色”外观的草图。在这个系统中,不可能产生不一致的父子关系,你可以在底部的测试代码中看到。

(我们将这些称为“红色”和“绿色”树,因为在白板上绘制数据结构时,这些是我们使用的标记颜色。)

using System;

interface IValue { string Value { get; } }
interface IParent : IValue { IChild Child { get; } }
interface IChild : IValue { IParent Parent { get; } }

abstract class HasValue : IValue
{
    private string value;
    public HasValue(string value)
    {
        this.value = value;
    }
    public string Value { get { return value; } }
}

sealed class GreenChild : HasValue
{
    public GreenChild(string value) : base(value) {}
}

sealed class GreenParent : HasValue
{
    private GreenChild child;
    public GreenChild Child { get { return child; } }
    public GreenParent(string value, GreenChild child) : base(value)
    { 
         this.child = child; 
    }

    public IParent MakeFacade() { return new RedParent(this); }

    private sealed class RedParent : IParent
    {
        private GreenParent greenParent;
        private RedChild redChild;
        public RedParent(GreenParent parent) 
        { 
            this.greenParent = parent; 
            this.redChild = new RedChild(this);
        }
        public IChild Child { get { return redChild; } }
        public string Value { get { return greenParent.Value; } }

        private sealed class RedChild : IChild
        {
            private RedParent redParent;
            public RedChild(RedParent redParent)
            {
                this.redParent = redParent;
            }
            public IParent Parent { get { return redParent; } }
            public string Value 
            { 
                get 
                { 
                    return redParent.greenParent.Child.Value; 
                } 
            }
        }
    }
}
class P
{
    public static void Main()
    {
        var greenChild1 = new GreenChild("child1");
        var greenParent1 = new GreenParent("parent1", greenChild1);
        var greenParent2 = new GreenParent("parent2", greenChild1);

        var redParent1 = greenParent1.MakeFacade();
        var redParent2 = greenParent2.MakeFacade();

        Console.WriteLine(redParent1.Value); // parent1
        Console.WriteLine(redParent1.Child.Parent.Value); // parent1 !
        Console.WriteLine(redParent2.Value); // parent2
        Console.WriteLine(redParent2.Child.Parent.Value); // parent2 !

        // See how that goes? RedParent1 and RedParent2 disagree on what the
        // parent of greenChild1 is, **but they are self-consistent**. They
        // always report that the parent of their child is themselves.
    }
}

答案 1 :(得分:11)

从技术角度来看,设计既不是特别糟糕也不是问题。类实例(对象)是引用类型,即_parent和_child仅保存对相应对象的引用,而不是对象本身。所以你不会导致一些无限的数据结构。

正如您自己发布的那样,垃圾收集器能够处理循环引用,主要是因为它不使用引用计数。

这种结构的唯一“问题”通常只是你经常想要保持关系的两端同步,即。给定Child c和Parent p然后p.Child == c if and only if c.Parent == p。您需要决定如何以最佳方式处理此问题。例如,如果你有一个方法Parent.AddChild(Child c),这不仅可以将Parent.Child设置为c,而且还可以将Child.Parent设置为父级。但是,如果直接分配Child.Parent呢?这些是您可能需要处理的一些问题。

答案 2 :(得分:3)

这是完全没错的IMO MS作为示例在大多数UI控件/元素中执行此操作,例如Form确实有一个Controls集合,该集合中的每个控件都有一个ParentForm属性。

答案 3 :(得分:2)

我认为这是正常的,如果您需要能够在两个方向上导航关系,那么这很有用。

正如您所指出的那样,GC可以处理它,那为什么它应该是一个问题呢?如果它提供了您需要的功能,那么应该没问题。

您应该警惕属性变得不同步的问题,假设这将成为我们代码的问题。