如何防止一次性手机扩散到你所有的班级?

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

假设我有一组这样的简单类:

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

一个 Bus有一个 DriverDriver有两个 Shoe,每个 Shoe有一个 Shoelace。都很傻。

将 IDisposable 对象添加到 Shoelace

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

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

实现一次性鞋带

但是现在 微软的 FxCop会抱怨: “在‘ Shoelace’上实现 IDisposable,因为它创建下列 IDisposable 类型的成员: ‘ EventWaitHandle’”

好吧,我在 Shoelace上实现了 IDisposable,然后我整洁的小类变成了这个可怕的混乱:

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

惊恐地看着一次性手机的传播

对,就是这样。但是现在 FxCop 会抱怨 Shoe创建了一个 Shoelace,所以 Shoe也必须是 IDisposable

Driver创建 Shoe,所以 Driver必须是 IDisposableBus创建 Driver,所以 Bus必须是 IDisposable,以此类推。

突然之间,我对 Shoelace的小改变给我带来了很多工作,我的老板想知道为什么我需要检查 Bus来改变 Shoelace

问题

如何防止 IDisposable的这种扩散,但仍然确保正确地处置非托管对象?

8760 次浏览

你不能真的“阻止”我一次性手机的传播。有些类需要处理,比如 AutoResetEvent,最有效的方法是在 Dispose()方法中处理,以避免终结器的开销。但是这个方法必须以某种方式调用,所以正如您的示例中一样,封装或包含 IDisposable 的类必须处理这些类,所以它们也必须是一次性的,等等。避免这种情况的唯一方法是:

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

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

这基本上就是将组合或聚合与 Disposable 类混合时发生的情况。正如前面提到的,第一种方法是重构鞋带外的 waitHandle。

尽管如此,当您没有非托管资源时,您可以大大减少 Disposable 模式。(我仍然在寻找这方面的官方参考。)

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

就正确性而言,如果父对象创建并且本质上拥有一个现在必须是可抛弃的子对象,那么您就不能通过对象关系阻止 IDisposable 的传播。在这种情况下,FxCop 是正确的,父节点必须是 IDisposable。

您可以做的是避免向对象层次结构中的叶类添加 IDisposable。这并不总是一个容易的任务,但它是一个有趣的练习。从逻辑的角度来看,鞋带没有理由是一次性的。这里不添加 WaitHandle,而是添加 ShoeLace 和 WaitHandle 之间的关联。最简单的方法是通过 Dictionary 实例。

如果您可以通过映射将 WaitHandle 移动到实际使用 WaitHandle 的位置,那么您可以中断这个链。

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

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

然后,当 ShoeIDisposable,FxCop (v1.36)不抱怨 Driver也应该是 IDisposable

然而,如果定义如下:

class Driver
{
Shoe leftShoe = new Shoe();
Shoe rightShoe = new Shoe();
}

然后它就会抱怨。

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

我不认为有一种技术方法可以防止 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
{
...
}

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

为了防止 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 抽象,您很快就会意识到它不应该受到基础设施依赖(如 IDisposable)的污染。IDisposable应该保留给那些抽象封装了需要确定性清理的资源的类; 也就是说,对于可丢弃性是 抽象的一部分的类。

用控制反转怎么样?

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