从这些简单的类开始......
假设我有一组简单的类,如下所示:
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
有一个 Driver
,Driver
有两个 Shoe
,每个 Shoe
有一个 Shoelace
。都非常傻。
将 IDisposable 对象添加到 Shoelace
后来我决定 Shoelace
上的某些操作可以是多线程的,所以我添加了一个 EventWaitHandle
以便线程与之通信。所以 Shoelace
现在看起来像这样:
class Shoelace
{
private AutoResetEvent waitHandle = new AutoResetEvent(false);
bool tied = false;
// ... other stuff ..
}
在鞋带上实现 IDisposable
但现在 Microsoft's 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 实现而不需要 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
的这种传播,但仍确保您的非托管对象得到正确处置?
你不能真正“阻止” IDisposable 传播。有些类需要被释放,例如 AutoResetEvent
,最有效的方法是在 Dispose()
方法中完成,以避免终结器的开销。但是这个方法必须以某种方式调用,所以就像在你的例子中一样,封装或包含 IDisposable 的类必须处理这些,所以它们也必须是一次性的,等等。避免它的唯一方法是:
尽可能避免使用 IDisposable 类,在单个位置锁定或等待事件,将昂贵的资源保存在单个位置等
仅在需要时创建它们并在之后处理它们(使用模式)
在某些情况下,可以忽略 IDisposable,因为它支持可选情况。例如,WaitHandle 实现 IDisposable 以支持命名的 Mutex。如果未使用名称,则 Dispose 方法不执行任何操作。 MemoryStream 是另一个例子,它不使用系统资源,而且它的 Dispose 实现也不做任何事情。仔细考虑是否正在使用非托管资源可能具有指导意义。因此可以检查 .net 库的可用源或使用反编译器。
就正确性而言,如果父对象创建并基本上拥有一个现在必须是一次性的子对象,则您无法阻止 IDisposable 通过对象关系传播。 FxCop 在这种情况下是正确的,并且父级必须是 IDisposable。
您可以做的是避免将 IDisposable 添加到对象层次结构中的叶类。这并不总是一件容易的事,但它是一个有趣的练习。从逻辑的角度来看,鞋带没有理由必须是一次性的。除了在此处添加 WaitHandle 之外,是否还可以在 ShoeLace 和 WaitHandle 的使用点之间添加关联。最简单的方法是通过 Dictionary 实例。
如果您可以在实际使用 WaitHandle 的点通过映射将 WaitHandle 移动到松散关联中,那么您可以断开此链。
为了防止 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
应保留给其抽象封装了需要确定性清理的资源的类;即,对于可处置性是抽象的一部分的类。
AutoResetEvent
用于在同一类中运行的不同线程之间进行通信,因此它必须是成员变量。您不能将其范围限制为方法。 (例如,假设一个线程通过阻塞 waitHandle.WaitOne()
等待某些工作。然后主线程调用 shoelace.Tie()
方法,该方法只执行 waitHandle.Set()
并立即返回)。
这基本上是当您将组合或聚合与一次性类混合时发生的情况。如前所述,第一个出路是用鞋带重构waitHandle。
话虽如此,当您没有非托管资源时,您可以大大减少 Disposable 模式。 (我仍在为此寻找官方参考。)
但是你可以省略析构函数和 GC.SuppressFinalize(this);并且可能稍微清理一下虚拟 void Dispose(bool disposing)。
有趣的是,如果 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
创建,并且仍需要以某种方式处理。
Shoe
构造函数之一中引发异常,那么 shoes
将处于困难状态?
如果您的设计保持如此紧密的耦合,我认为没有一种技术方法可以防止 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
{
...
}
不幸的是,新设计更加复杂,因为它需要额外的类来实例化和处理鞋子和驱动程序的具体实例,但这种复杂性是要解决的问题所固有的。好消息是,公交车不再需要仅仅为了处理鞋带而被丢弃。
如何使用控制反转?
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))}));
}
}
}