如何防止IDisposable传播到您的所有班级?


138

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

假设我有一组简单的类,如下所示:

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;
}

A Bus有一个Driver,A 有Driver两个Shoe,每个Shoe都有一个Shoelace。一切都很傻。

将IDisposable对象添加到Shoelace

后来我决定对进行某些操作Shoelace可以是多线程的,因此我EventWaitHandle为线程添加了一个与之通信。所以Shoelace现在看起来像这样:

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

在鞋带上实现IDisposable

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

好吧,我实现了IDisposableShoelace和我的整洁的小类成为这个乌七八糟:

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)and析构函数:

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创建。DriverBusIDisposable

突然我的零钱Shoelace引起了我很多工作,我的老板想知道为什么我需要结账Bus才能更改Shoelace

问题

如何防止扩散IDisposable,但仍要确保正确处理非托管对象?


9
我认为这是一个非常好的问题,答案是最大程度地减少其使用并尝试使高级IDisposables在使用时短暂存在,但这并不总是可能的(尤其是那些IDisposables是由于与C ++ dll或类似操作互操作而引起的)。展望未来。
annakata

好的,丹,我已经更新了问题,以显示在Shoelace上实现IDisposable的两种方法。
GrahamS

我通常会警惕依靠其他类的实现细节来保护我。如果我可以轻松地预防它,那就没有风险了。也许我过于谨慎,也许我只是花了太长时间担任C程序员,但我还是想采用爱尔兰的方法:“可以肯定,可以肯定” :)
GrahamS 2009年

@Dan:仍然需要进行null检查以确保未将对象本身设置为null,在这种情况下,对waitHandle.Dispose()的调用将引发NullReferenceException。
Scott Dorman

1
无论如何,您实际上应该仍然使用Dispose(bool)方法,如“在鞋带上实现IDisposable”部分中所示,因为(减去finalizer)是完整模式。仅仅因为一个类使IDisposable失效,并不意味着它需要一个终结器。
Scott Dorman

Answers:


36

您不能真正“防止” IDisposable扩散。需要处理一些类,例如AutoResetEvent,而最有效的方法是在Dispose()方法中这样做,以避免终结器的开销。但是必须以某种方式调用此方法,因此就像在您的示例中一样,封装或包含IDisposable的类必须处置它们,因此它们也必须是可抛弃的,等等。唯一避免这种情况的方法是:

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

在某些情况下,IDisposable可忽略,因为它支持可选情况。例如,WaitHandle实现IDisposable以支持命名的Mutex。如果未使用名称,则Dispose方法不执行任何操作。MemoryStream是另一个示例,它不使用任何系统资源,并且其Dispose实现也不执行任何操作。仔细考虑是否正在使用非托管资源可能具有指导意义。因此,可以检查.net库的可用资源或使用反编译器。


4
因为今天的实现不能做任何事情而可以忽略它的建议是不好的,因为将来的版本实际上可能在Dispose中做一些重要的事情,现在您很难追踪泄漏。
安迪

1
“保留昂贵的资源”,IDisposable的资源不一定昂贵,实际上,使用偶数等待句柄的此示例在您获得时就“轻巧”。
markmnl

20

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

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

如果您可以在实际使用WaitHandle的位置通过地图将WaitHandle移到松散的关联中,则可以断开此链。


2
在那建立联系感觉很奇怪。此AutoResetEvent对Shoelace实现是私有的,因此在我看来公开公开它是错误的。
GrahamS

@GrahamS,我不是说要公开公开它。我是说可以移动到绑鞋带的地步。也许如果绑鞋带是一项复杂的任务,那么他们应该属于鞋带阶层。
JaredPar

1
您可以使用一些代码片段JaredPar扩展答案吗?我可能在扩展我的示例,但是我想象到Shoelace创建并启动了Tyer线程,该线程耐心地在waitHandle.WaitOne()中等待。
GrahamS

1
+1。我认为仅同时调用Shoelace而不同时调用其他人会很奇怪。考虑什么应该是聚合根。在这种情况下,它应该是Bus,恕我直言,尽管我对域不熟悉。因此,在那种情况下,总线应包含waithandle,并且针对总线的所有操作及其所有子节点都将被同步。
约翰内斯·古斯塔夫松

16

为防止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方法,并且该类不需要具有可抛弃的字段,因此也不需要是可抛弃的本身。

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


1
我完全同意,Shoelace的实现细节不应污染公共接口,但我的观点是,它很难避免。您在这里提出的建议并非总是可行的:AutoResetEvent()的目的是在线程之间进行通信,因此它的范围通常会扩展到单个方法之外。
GrahamS 2010年

@GrahamS:这就是为什么我说要尝试以这种方式设计的原因。您也可以将一次性用品传递给其他方法。仅当外部调用类控制一次性用品的生命周期时,它才会崩溃。我将相应地更新答案。
Jordão酒店

2
抱歉,我知道您可以随意丢弃一次性物品,但我仍然看不到这种方法。在我的示例中,AutoResetEvent它用于在同一类中运行的不同线程之间进行通信,因此它必须是成员变量。您不能将其范围限制为方法。(例如,假设一个线程只是通过阻塞来等待一些工作waitHandle.WaitOne()。然后主线程调用shoelace.Tie()方法,该方法只执行a waitHandle.Set()并立即返回)。
GrahamS 2010年

3

当您将“组合”或“聚合”与“一次性”类混合使用时,基本上就是这种情况。如前所述,第一种方法是从鞋带中重构waitHandle。

话虽如此,当您没有非托管资源时,可以大大简化Disposable模式。(我仍在为此寻找官方参考。)

但是您可以省略析构函数和GC.SuppressFinalize(this); 也许可以稍微清理一下虚拟虚空Dispose(布尔处理)。


谢谢亨克。如果我把WaitHandle中创建了鞋带,然后有人仍然要处理废弃的地方怎么这么会说有人知道,鞋带是完成了它(和鞋带没有它传递给任何其他类)?
GrahamS

您不仅需要移动“创建”,还需要移动“等待”的责任。jaredPar的答案从一个有趣的想法开始。
汉克·霍尔特曼

该博客介绍了实现IDisposable的方法,特别是解决了如何通过避免在单个类Blog中
2009/

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并且仍需要以某种方式进行处置。


2
如果Show实现IDisposable,则在字段初始化程序中创建它是危险的。有趣的是FXCop不允许这样的初始化,因为在C#中,安全地进行初始化的唯一方法是使用ThreadStatic变量(完全隐藏)。在vb.net中,可以在没有ThreadStatic变量的情况下安全地初始化IDisposable对象。该代码仍然很丑陋,但并不是那么丑陋。我希望MS将提供一种安全使用此类字段初始化程序的好方法。
supercat

1
@supercat:谢谢。你能解释为什么它很危险吗?我猜想,如果在Shoe构造函数之一中引发异常,那么shoes它将处于困难状态吗?
GrahamS

3
除非有人知道可以安全地放弃特定类型的IDisposable,否则应确保所创建的每个对象都被处置。如果在对象的字段初始化器中创建了一个IDisposable,但在完全构造该对象之前引发了异常,则该对象及其所有字段将被放弃。即使将对象的构造包装在使用“ try”块检测到构造失败的工厂方法中,工厂也很难获得对需要处理的对象的引用。
supercat

3
我知道在C#中处理该问题的唯一方法是使用ThreadStatic变量来保留对象列表,如果当前构造函数抛出该对象,则需要处理这些对象。然后,让每个字段初始化器在创建对象时注册每个对象,如果工厂成功完成,则让工厂清除列表,如果未清除,则使用“ finally”子句从列表中清除所有项目。那将是一个可行的方法,但是ThreadStatic变量很丑陋。在vb.net中,可以使用类似的技术,而无需任何ThreadStatic变量。
supercat

3

如果您保持紧密的设计耦合,我认为没有防止IDisposable传播的技术方法。然后应该怀疑设计是否正确。

在您的示例中,我认为让鞋拥有鞋带是有意义的,也许驾驶员应该拥有自己的鞋。但是,总线不应该拥有驱动程序。通常,公交车司机不会跟随公交车到废品场:)就司机和鞋子而言,他们很少自己制造鞋子,这意味着他们并不是真正“拥有”鞋子。

另一种设计可以是:

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
{
   ...
}

不幸的是,新设计更加复杂,因为它需要额外的类来实例化和处理鞋子和驾驶员的具体实例,但是这种复杂性是要解决的问题所固有的。好处是公交车不再仅仅是为了处理鞋带而必须是一次性的。


2
但是,这并不能真正解决原始问题。它可能会使FxCop保持安静,但仍应丢弃鞋带。
AndrewS 2010年

我认为您所做的只是将问题转移到其他地方。ShoePairWithDisposableLaces现在拥有Shoelace(),因此还必须使其具有IDisposable-那么谁来处置这双鞋子?您是否将此留给某些IoC容器处理?
GrahamS 2010年

@AndrewS:确实,仍然必须丢弃驾驶员,鞋子和鞋带,但不应由公共汽车发起处置。@GrahamS:没错,我将问题转移到其他地方,因为我认为它属于其他地方。通常,会有一类实例化鞋子,这也将负责处理鞋子。我还编辑了代码,使ShoePairWithDisposableLaces也成为一次性的。
荷兰Joh

@Joh:谢谢。正如您所说,将其移至其他位置的问题是,您将创建更多的复杂性。如果ShoePairWithDisposableLaces是由其他某个类创建的,那么当驱动程序完成其Shoes时,是否不必通知该类,以便可以正确地Dispose()?
GrahamS 2010年

1
@Graham:是的,将需要某种通知机制。例如,可以使用基于参考计数的处置策略。还有一些附加的复杂性,但你可能知道,“没有这样的事,作为一个免费擦鞋” :)
荷兰Joh

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))}));
        }
    }
}

现在您已经解决了调用者问题,Shoelace不能依赖于需要它时可用的等待句柄。
安迪

@andy您的意思是“ Shoelace不能依赖于在需要时可用的等待句柄”?它被传递给构造函数。
达拉格

在将autoresetevents状态提供给Shoelace之前,其他原因可能会使它感到混乱,并且它可能始于ARE处于不良状态;在Shoelace正在执行其操作时,可能会导致ARE的状态发生其他变化,从而导致Shoelace执行错误的操作。与您仅锁定私人成员的原因相同。“通常,..避免锁定超出代码控制范围的实例” docs.microsoft.com/en-us/dotnet/csharp/language-reference/…–
Andy

我同意只锁定私人成员。但是似乎等待句柄是不同的。实际上,将它们传递给对象似乎更有用,因为需要使用同一实例跨线程进行通信。尽管如此,我认为IoC为OP的问题提供了有用的解决方案。
达拉格

假设OP的示例将waithandle作为私有成员,则可以安全地假设该对象希望对其进行独占访问。使该句柄在实例外部可见是违反该规则的。通常,IoC可以对诸如此类的事情有所帮助,但对于线程处理则无济于事。
安迪
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.