我已经仔细阅读了this的文章,并且似乎清楚地指出在所有IDisposable
实现的情况下都应该实现处置模式。我试图了解为什么在我的班级仅包含托管资源(即其他IDisposable
成员或安全句柄)的情况下需要实现处置模式的原因。为什么我不能写
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();
}
}
更重要的是,对于我来说,当实现仅对他们所知道的事情负责时,看起来更好地分离了关注点。
答案 0 :(得分:5)
此
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的重载阻止终结器尝试执行以下操作: 释放可能已经回收的托管资源。
访问已回收的托管对象,或在其被处置后访问其属性(可能是由另一个终结器),将导致在终结器中引发异常,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()但是boo可能已被GC回收。
答案 1 :(得分:1)
我还没有看到Dispose
的这种特殊用法,所以我想指出一个不使用处理方式的常见内存泄漏源。
Visual Studio 2017实际上是通过静态代码分析对此抱怨的,我应该“实现处置模式”。请注意,我正在使用SonarQube和SolarLint,而且我不认为Visual Studio会单独解决这一问题。 FxCop(另一种静态代码分析工具)可能会,尽管我没有对此进行测试。
我注意到下面的代码展示了处置模式,该代码也可以防止类似的情况,即没有不受管理的资源:
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
也应该退订该活动。但这确实突出了一种常见的情况,即事件 subscriber 超出了 publisher 。
我已经看到事件导致无数内存泄漏。尤其是在超大型或超高性能代码库中。
我也很难数出我看到物体存活超过处置时间的次数。这是许多探查器发现内存泄漏(某些GC根仍然保留的处置对象)的一种非常常见的方式。
再次,过于简化的示例和可怕的代码。但是,对对象调用Dispose
并不期望它处置整个对象(无论是否源自一百万次)都不是一种好习惯。
修改
请注意,此答案是有意仅解决托管资源的问题,这表明处置模式在这种情况下也很有用。这有目的地不解决非托管资源的用例,因为我觉得缺乏对仅托管用途的关注。而且,这里还有很多其他好的答案。
但是,我将注意一些有关非托管资源的重要提示。上面的代码可能无法解决非托管资源,但是我想表明它与应该应如何处理不矛盾。
当类负责非托管资源时,使用finalizers非常重要。简短地说,垃圾收集器会自动调用终结器。因此,它为您提供合理的保证,使其始终在某个时间点被调用。它不是防弹的,但是与希望用户代码调用Dispose
相去甚远。
对于Dispose
,此保证是 not 正确的。 GC可以回收对象,而无需调用Dispose
。这就是终结器用于非托管资源的关键原因。 GC本身仅处理托管资源。
但是我还要指出,不应用于清理托管资源同样重要。为什么有很多原因(毕竟这是GC的工作),但是使用终结器的最大缺点之一就是延迟了对象的垃圾回收。
GC看到对象可以自由回收但具有终结器,它将通过将对象放置在终结器队列中来延迟收集。这样会增加对象不必要的使用寿命,并对GC造成更大压力。
最后,我要指出的是,尽管具有与C ++中的析构函数类似的语法,但由于此原因,终结器是不确定的。它们是非常不同的野兽。您绝对不应在特定时间依赖终结器来清理非托管资源。
答案 2 :(得分:0)
您可能理解错了。如果您没有非托管资源,则无需实施finilizer。您可以通过在Visual Studio中使用自动模式实现来检查它(它甚至会生成注释,说只有在使用非托管资源的情况下,才应该取消注释终结器)。
处置模式仅用于访问非托管资源的对象。
如果您设计基类,并且某些继承类访问非托管资源,那么继承类将通过覆盖Dispose(bool)
并定义终结器来自行处理。
this文章对此进行了解释,如果不取消抑制,则将最终调用所有终结器。而且,如果抑制了一切,那么最初的Diapose(true)
调用链会释放所有内容。
答案 3 :(得分:-1)
如果Dispose()
是使用公共虚拟方法实现的,则希望重写该方法的派生类可以这样做,并且一切都会很好。但是,如果继承链上的任何内容通过重写公共虚拟IDisposable.Dispose()
方法之外的方法来实现Dispose()
,则可能导致子派生类无法实现自己的IDisposable.Dispose()
同时仍然能够访问父级实现。
可以使用Dispose(bool)
模式,而不管是否有公共Dispose()
方法,因此避免了在类公开或不公开类的情况下需要使用单独的模式公用Dispose()
方法。 GC.SuppressFinalize(this)
通常可以用GC.KeepAlive(this)
代替,但是对于没有终结器的类,其成本大致相同。如果没有该调用,则在类自己的Dispose
方法运行时,可以触发类持有引用的任何对象的终结器。不太可能出现这种情况,即使发生也不会导致问题的通常情况,但是将this
传递给GC.KeepAlive(Object)
或GC.SuppressFinalize(Object)
使得这种古怪的情况成为不可能。