我仔细阅读了this文章,它似乎清楚地表明应该在IDisposable
实现的所有情况下实施处置模式。我试图理解为什么我需要在我的类只保存托管资源(即其他IDisposable
成员或安全句柄)的情况下实现dispose模式。为什么我不能写
class Foo : IDisposable
{
IDisposable boo;
void Dispose()
{
boo?.Dispose();
}
}
如果肯定知道没有非托管资源,并且没有必要从终结器调用Dispose
方法,因为托管资源没有从终结器中释放出来?
更新:为了增加一些清晰度。讨论似乎归结为是否需要为每个实现IDisposable
的基本公共非密封类实现处置模式的问题。但是,当没有非托管资源的基类不使用dispose模式而具有非托管资源的子类确实使用此模式时,我无法找到层次结构的潜在问题:
class Foo : IDisposable
{
IDisposable boo;
public virtual void Dispose()
{
boo?.Dispose();
}
}
// child class which holds umanaged resources and implements dispose pattern
class Bar : Foo
{
bool disposed;
IntPtr unmanagedResource = IntPtr.Zero;
~Bar()
{
Dispose(false);
}
public override void Dispose()
{
base.Dispose();
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposed)
return;
if (disposing)
{
// Free any other managed objects here.
//
}
// close handle
disposed = true;
}
}
// another child class which doesn't hold unmanaged resources and merely uses Dispose
class Far : Foo
{
private IDisposable anotherDisposable;
public override void Dispose()
{
base.Dispose();
anotherDisposable?.Dispose();
}
}
更重要的是,对我而言,当实现仅对他们所知道的事情负责时,它看起来更好地分离了关注点。
这个
private class Foo : IDisposable
{
IDisposable boo;
public void Dispose()
{
boo?.Dispose();
}
}
完全没问题。原样
public sealed class Foo : IDisposable
{
IDisposable boo;
public void Dispose()
{
boo?.Dispose();
}
}
如果我使用虚拟Dispose方法实现如上所述的公共密封基类,会出现什么问题?
来自docs:
由于未定义垃圾收集器在完成期间销毁托管对象的顺序,因此调用此Dispose重载值为false会阻止终结器尝试释放可能已经回收的托管资源。
访问已经回收的托管对象,或者在Disposed之后访问其属性(可能是由另一个终结器)将导致在Finalizer中引发异常,即bad:
如果Finalize或Finalize的覆盖引发异常,并且运行时不由覆盖默认策略的应用程序托管,则运行时将终止该进程,并且不会执行活动的try / finally块或终结器。如果终结器无法释放或销毁资源,则此行为可确保进程完整性。
所以如果你有:
public class Foo : IDisposable
{
IDisposable boo;
public virtual void Dispose()
{
boo?.Dispose();
}
}
public class Bar : Foo
{
IntPtr unmanagedResource = IntPtr.Zero;
~Bar()
{
this.Dispose();
}
public override void Dispose()
{
CloseHandle(unmanagedResource);
base.Dispose();
}
void CloseHandle(IntPtr ptr)
{
//whatever
}
}
〜Bar - > Bar.Dispose() - > base.Dispose() - > boo.Dispose()但是GC可能已经回收了boo。
我还没有看到提到的Dispose
的这种特殊用法,所以我想在不使用dispose模式时我会指出一个常见的内存泄漏源。
Visual Studio 2017实际上是通过静态代码分析来抱怨我应该“实现配置模式”。请注意我正在使用SonarQube和SolarLint,我不相信Visual Studio会单独捕获它。 FxCop(另一种静态代码分析工具)可能会,尽管我没有测试过。
我注意到下面的代码展示了dispose模式也可以防止像这样的东西,它没有非托管资源:
public class Foo : IDisposable
{
IDisposable boo;
public void Dispose()
{
boo?.Dispose();
}
}
public class Bar : Foo
{
//Memory leak possible here
public event EventHandler SomeEvent;
//Also bad code, but will compile
public void Dispose()
{
someEvent = null;
//Still bad code even with this line
base.Dispose();
}
}
以上说明了非常糟糕的代码。不要这样做。为什么这个可怕的代码?那是因为:
Foo foo = new Bar();
//Does NOT call Bar.Dispose()
foo.Dispose();
让我们假设这个可怕的代码暴露在我们的公共API中。考虑消费者使用的上述类:
public sealed class UsesFoo : IDisposable
{
public Foo MyFoo { get; }
public UsesFoo(Foo foo)
{
MyFoo = foo;
}
public void Dispose()
{
MyFoo?.Dispose();
}
}
public static class UsesFooFactory
{
public static UsesFoo Create()
{
var bar = new Bar();
bar.SomeEvent += Bar_SomeEvent;
return new UsesFoo(bar);
}
private static void Bar_SomeEvent(object sender, EventArgs e)
{
//Do stuff
}
}
消费者是否完美?不.... UsesFooFactory
应该也可以取消订阅此事件。但它确实突出了事件订阅者超过发布者的常见情况。
我见过事件导致无数的内存泄漏。特别是在非常大或极端高性能的代码库中。
我几乎无法计算有多少次我看到物品在他们处置之后很久就存活了。这是一种非常常见的方式,许多分析器发现内存泄漏(处置的对象仍由某种GC根保持)。
再一次,过于简化的例子和可怕的代码。但是,在一个物体上调用Dispose
并且不要指望它处理整个物体,无论它是否已经衍生出一百万次,实际上从来都不是好习惯。
编辑
请注意,这个答案是故意仅针对托管资源,展示了配置模式在这种情况下也很有用。这有目的地没有解决非托管资源的用例,因为我觉得缺乏对仅管理的用途的关注。在这里还有许多其他好的答案可以谈论这个问题。
但是,我会注意到一些对非托管资源很重要的快速事项。上面的代码可能无法解决非托管资源,但我想说清楚它与如何处理它们并不矛盾。
当你的班负责非托管资源时,使用finalizers非常重要。简而言之,垃圾收集器会自动调用终结器。所以它给你一个合理的保证,它总是在某个时间点被调用。它不是防弹的,但与希望用户代码调用Dispose
相去甚远。
这种保证不适用于Dispose
。在没有调用Dispose
的情况下,GC可以回收一个对象。这是终结器用于非托管资源的关键原因。 GC本身只处理托管资源。
但我还要注意,同样重要的终结器不应该用于清理托管资源。有无数的原因(毕竟这是GC的工作),但使用终结器的最大缺点之一是延迟对象的垃圾收集。
GC看到一个对象可以自由回收但是有一个终结器,它会通过将对象放在终结器队列中来延迟收集。这会给对象增加不必要的生命周期,加上对GC的压力更大。
最后我会注意到终结器因为这个原因是非确定性的,尽管它具有与C ++中的析构函数类似的语法。他们是非常不同的野兽。您永远不应该依赖终结器来清理特定时间点的非托管资源。
你可能错了。如果您没有非托管资源,则无需实现finilizer。您可以通过在Visual Studio中使用自动模式实现来检查它(它甚至会生成注释,说明如果使用非托管资源,则只应取消注释终结器)。
dispose模式仅用于访问非托管资源的对象。
如果您设计基类并且某些继承类访问非托管资源,则继承类将通过重写Dispose(bool)
并定义终结器来自行处理它。
它在this文章中解释过,如果没有被压制,所有终结器都会被调用。如果被压制,一切都将由Diapose(true)
电话链首先释放。
如果使用公共虚方法实现Dispose()
,那么期望覆盖该方法的派生类可以这样做,并且一切都会很好。但是,如果继承链上的任何内容通过除了覆盖公共虚拟IDisposable.Dispose()
方法之外的方式实现Dispose()
,则可能使子派生类无法实现其自己的IDisposable.Dispose()
,同时仍能够访问父实现。
无论是否存在公共Dispose(bool)
方法,都可以使用Dispose()
模式,因此避免了对于类是否暴露公共Dispose()
方法的情况需要单独的模式。 GC.SuppressFinalize(this)
通常可以用GC.KeepAlive(this)
代替,但对于没有终结器的类,成本大致相同。在没有该调用的情况下,当类自己的Dispose
方法运行时,类可以触发引用的任何对象的终结器。不是一个可能的场景,也不会出现通常会导致问题的情况,但是将this
传递给GC.KeepAlive(Object)
或GC.SuppressFinalize(Object)
会让这种奇怪的情况变得不可能。