你如何防止IDisposable传播到你的所有课程?

时间:2009-03-19 11:13:24

标签: c# .net garbage-collection dispose idisposable

从这些简单的类开始......

假设我有一组简单的类:

class Bus
{
    Driver busDriver = new Driver();
}

class Driver
{
    Shoe[] shoes = { new Shoe(), new Shoe() };
}

class Shoe
{
    Shoelace lace = new Shoelace();
}

class Shoelace
{
    bool tied = false;
}

BusDriverDriverShoe个,ShoeShoelace。一切都很傻。

将一个IDisposable对象添加到Shoelace

后来我决定Shoelace上的某些操作可能是多线程的,所以我为要与之通信的线程添加EventWaitHandle。所以Shoelace现在看起来像这样:

class Shoelace
{
    private AutoResetEvent waitHandle = new AutoResetEvent(false);
    bool tied = false;
    // ... other stuff ..
}

在Shoelace上实施IDisposable

但现在Microsoft's FxCop会抱怨:“在'Shoelace'上实施IDisposable,因为它会创建以下IDisposable类型的成员:'EventWaitHandle'。”

好的,我在IDisposable上实施了Shoelace,我整洁的小班变成了这个可怕的混乱:

class Shoelace : IDisposable
{
    private AutoResetEvent waitHandle = new AutoResetEvent(false);
    bool tied = false;
    private bool disposed = false;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    ~Shoelace()
    {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            if (disposing)
            {
                if (waitHandle != null)
                {
                    waitHandle.Close();
                    waitHandle = null;
                }
            }
            // No unmanaged resources to release otherwise they'd go here.
        }
        disposed = true;
    }
}

或者(由评论者指出)由于Shoelace本身没有非托管资源,我可能会使用更简单的dispose实现,而不需要Dispose(bool)和析构函数:

class Shoelace : IDisposable
{
    private AutoResetEvent waitHandle = new AutoResetEvent(false);
    bool tied = false;

    public void Dispose()
    {
        if (waitHandle != null)
        {
            waitHandle.Close();
            waitHandle = null;
        }
        GC.SuppressFinalize(this);
    }
}

恐惧地看着IDisposable传播

是的,那是固定的。但现在FxCop会抱怨Shoe创建了Shoelace,因此Shoe也必须IDisposable

Driver创建Shoe,因此Driver必须为IDisposable。 并Bus创建Driver,因此Bus必须为IDisposable,依此类推。

突然间,我对Shoelace的小改变让我做了很多工作,而我的老板想知道为什么我需要结帐Bus才能更改Shoelace

问题

如何阻止IDisposable的传播,但仍然确保您的非托管对象得到妥善处置?

8 个答案:

答案 0 :(得分:34)

你无法真正“阻止”IDisposable蔓延。有些类需要处理,如AutoResetEvent,最有效的方法是在Dispose()方法中执行,以避免终结器的开销。但是必须以某种方式调用此方法,因此在您的示例中,封装或包含IDisposable的类必须处理这些,因此它们也必须是一次性的,等等。避免它的唯一方法是:

  • 尽可能避免使用IDisposable类,在单个位置锁定或等待事件,在单个位置保留昂贵的资源等。
  • 仅在您需要时创建它们并在(using模式)
  • 之后处理它们

在某些情况下,可以忽略IDisposable,因为它支持可选案例。例如,WaitHandle实现IDisposable以支持命名的Mutex。如果未使用名称,则Dispose方法不执行任何操作。 MemoryStream是另一个例子,它不使用系统资源,它的Dispose实现也什么都不做。关于是否使用非托管资源的仔细思考可以是教学性的。因此,可以检查.net库的可用来源或使用反编译器。

答案 1 :(得分:19)

就正确性而言,如果父对象创建并且基本上拥有现在必须是一次性的子对象,则无法阻止IDisposable通过对象关系传播。在这种情况下,FxCop是正确的,父级必须是IDisposable。

您可以做的是避免将IDisposable添加到对象层次结构中的叶类。这并不总是一件容易的事,但这是一项有趣的练习。从逻辑的角度来看,ShoeLace没有必要是一次性的。除了在这里添加WaitHandle之外,还可以在ShoeLace和WaitHandle之间添加一个关联点。最简单的方法是通过Dictionary实例。

如果你可以通过实际使用WaitHandle的地图将WaitHandle移动到松散关联中,那么你可以打破这个链。

答案 2 :(得分:14)

为了防止IDisposable传播,您应该尝试在单个方法中封装一次性对象的使用。尝试以不同方式设计Shoelace

class Shoelace { 
  bool tied = false; 

  public void Tie() {

    using (var waitHandle = new AutoResetEvent(false)) {

      // you can even pass the disposable to other methods
      OtherMethod(waitHandle);

      // or hold it in a field (but FxCop will complain that your class is not disposable),
      // as long as you take control of its lifecycle
      _waitHandle = waitHandle;
      OtherMethodThatUsesTheWaitHandleFromTheField();

    } 

  }
} 

等待句柄的范围仅限于Tie方法,并且该类不需要具有一次性字段,因此本身不需要是一次性的。

由于wait句柄是Shoelace内部的实现细节,因此它不应以任何方式更改其公共接口,例如在其声明中添加新接口。当你不再需要一次性领域时会发生什么,你会删除IDisposable声明吗?如果您考虑Shoelace 抽象,您很快就会意识到它不应该被基础结构依赖性污染,例如IDisposableIDisposable应该保留给那些抽象封装了一个需要确定性清理的资源的类;即,对于可处置性是抽象的一部分的类。

答案 3 :(得分:3)

这感觉很像是一个更高级别的设计问题,通常是“快速修复”陷入泥潭的情况。有关解决方法的更多讨论,您可能会发现this thread有帮助。

答案 4 :(得分:3)

有趣的是,如果Driver定义如上:

class Driver
{
    Shoe[] shoes = { new Shoe(), new Shoe() };
}

然后当Shoe成为IDisposable时,FxCop(v1.36)不会抱怨Driver也应该是IDisposable

但如果定义如下:

class Driver
{
    Shoe leftShoe = new Shoe();
    Shoe rightShoe = new Shoe();
}
然后它会抱怨。

我怀疑这只是FxCop的限制,而不是解决方案,因为在第一个版本中,Shoe实例仍由Driver创建,但仍需要以某种方式处理。

答案 5 :(得分:3)

如果您保持设计紧密耦合,我认为没有一种技术方法可以防止IDisposable蔓延。然后人们应该想知道设计是否正确。

在你的例子中,我认为让鞋子拥有鞋带是有意义的,也许,司机应该拥有他/她的鞋子。但是,总线不应该拥有驱动程序。通常情况下,公共汽车司机不会跟随公共汽车去scrapyard :)在司机和鞋子的情况下,司机很少自己制鞋,这意味着他们并不真正“拥有”它们。

另一种设计可能是:

class Bus
{
   IDriver busDriver = null;
   public void SetDriver(IDriver d) { busDriver = d; }
}

class Driver : IDriver
{
   IShoePair shoes = null;
   public void PutShoesOn(IShoePair p) { shoes = p; }
}

class ShoePairWithDisposableLaces : IShoePair, IDisposable
{
   Shoelace lace = new Shoelace();
}

class Shoelace : IDisposable
{
   ...
}

不幸的是,新设计更加复杂,因为它需要额外的类来实例化和处理鞋子和驱动器的具体实例,但这种复杂性是解决问题所固有的。好处是公共汽车不再需要一次性处理,只是为了处理鞋带。

答案 6 :(得分:2)

这基本上是将Composition或Aggregation与Disposable类混合时会发生的情况。如上所述,第一个出路是将waitHandle重新制成鞋带。

话说回来,当你没有非托管资源时,可以大大删除Disposable模式。 (我仍然在寻找官方参考资料。)

但是你可以省略析构函数和GC.SuppressFinalize(this);并且可能会清理虚拟空隙Dispose(bool disposing)。

答案 7 :(得分:1)

如何使用控制反转?

class Bus
{
    private Driver busDriver;

    public Bus(Driver busDriver)
    {
        this.busDriver = busDriver;
    }
}

class Driver
{
    private Shoe[] shoes;

    public Driver(Shoe[] shoes)
    {
        this.shoes = shoes;
    }
}

class Shoe
{
    private Shoelace lace;

    public Shoe(Shoelace lace)
    {
        this.lace = lace;
    }
}

class Shoelace
{
    bool tied;
    private AutoResetEvent waitHandle;

    public Shoelace(bool tied, AutoResetEvent waitHandle)
    {
        this.tied = tied;
        this.waitHandle = waitHandle;
    }
}

class Program
{
    static void Main(string[] args)
    {
        using (var leftShoeWaitHandle = new AutoResetEvent(false))
        using (var rightShoeWaitHandle = new AutoResetEvent(false))
        {
            var bus = new Bus(new Driver(new[] {new Shoe(new Shoelace(false, leftShoeWaitHandle)),new Shoe(new Shoelace(false, rightShoeWaitHandle))}));
        }
    }
}