是否真的需要仅对托管资源实现释放模式



我已经仔细阅读了这篇文章,它似乎清楚地表明,在所有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();
}
}

更重要的是,对我来说,当实现只负责他们知道的那些事情时,它看起来是更好的关注点分离。

private class Foo : IDisposable
{
IDisposable boo;
public void Dispose()
{
boo?.Dispose();
}
}

完全没问题。 原样

public sealed class Foo : IDisposable
{
IDisposable boo;
public void Dispose()
{
boo?.Dispose();
}
}

如果我使用虚拟 Dispose 方法实现上述公共未密封基类,可能会出现什么问题?

从文档中:

因为垃圾回收器销毁的顺序托管 未定义定版期间的对象,调用此 Dispose 值为 false 的重载可防止终结器尝试 释放可能已回收的托管资源。

访问已被回收的托管对象,或在释放后访问其属性(可能由另一个终结器)将导致在终结器中引发异常,这是不好的:

如果"完成"或"完成"的替代引发异常,并且 运行时不是由覆盖默认值的应用程序承载的 策略,运行时终止进程,并且没有活动的尝试/最后 执行块或终结器。此行为可确保进程 终结器无法释放或销毁资源时的完整性。

因此,如果您有:

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 回收。

我还没有看到Dispose提到的这种特殊用法,所以我想在不使用 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可能也应该取消订阅该活动。 但它确实突出了一个常见方案,即事件订阅者的寿命超过发布者

我见过导致无数内存泄漏的事件。 特别是在非常大或超高性能的代码库中。

我也很难数出有多少次我看到物体在处理时间过了很长时间。 这是许多探查器查找内存泄漏(释放的对象仍由某种 GC 根保留)的一种非常常见的方式。

再次,过于简化的示例和可怕的代码。 但是,对一个对象调用Dispose而不期望它释放整个对象,无论它是否派生自一百万次,这真的从来都不是好的做法。

编辑

请注意,此答案有意仅针对托管资源,表明释放模式在此方案中也很有用。 这故意不解决非托管资源的用例,因为我觉得缺乏对仅托管用途的关注。 这里还有许多其他很好的答案来谈论这个问题。

但是,我将指出一些在涉及非托管资源时很重要的快速事项。 上面的代码可能无法解决非托管资源,但我想明确指出,它与如何处理它们并不矛盾。

当您的类负责非托管资源时,使用终结器非常重要。 简而言之,终结器由垃圾回收器自动调用。 因此,它为您提供了一个合理的保证,即它总是在某个时间点被调用。 它不是万无一失的,但与希望用户代码调用Dispose相去甚远。

这种保证不适用于Dispose。 GC 可以回收对象,而无需调用Dispose。 这是终结器用于非托管资源的关键原因。 GC 本身仅处理托管资源。

但我还要指出,同样重要的是,不应使用终结器来清理托管资源。 原因不计其数(毕竟这是 GC 的工作),但使用终结器的最大缺点之一是延迟对象的垃圾回收。

GC 看到对象可以自由回收但具有终结器,将通过将对象放入终结器队列来延迟收集。 这大大增加了物体不必要的生存期,并增加了GC的压力。

最后,我要指出,由于这个原因,终结器是不确定的,尽管它的语法与 C++ 中的析构函数类似。 它们是非常不同的野兽。 切勿依赖终结器在特定时间点清理非托管资源。

你可能弄错了。如果没有非托管资源,则无需实现 finilizer。你可以通过使用Visual Studio中的自动模式实现来检查它(它甚至会生成注释,说只有当你使用非托管资源时,才应该取消注释终结器)。

释放模式仅用于访问非托管资源的对象。

如果设计基类并且某些继承类访问非托管资源,则继承类将通过重写Dispose(bool)并定义终结器来自行处理它。

本文对此进行了解释,如果不抑制,无论如何都会调用所有终结器。如果被压制,一切都会首先被Diapose(true)呼叫链释放。

如果使用公共虚拟方法实现Dispose(),则期望重写该方法的派生类可以这样做,并且一切都会很好。 但是,如果继承链上的任何内容通过重写公共虚拟Dispose()方法以外的方式实现IDisposable.Dispose(),则可能导致子派生类无法实现自己的IDisposable.Dispose()同时仍能够访问父实现。

无论是否存在公共Dispose()方法,都可以使用Dispose(bool)模式,从而避免了在类公开或不公开公共Dispose()方法的情况下使用单独的模式的需要。GC.SuppressFinalize(this)通常可以用GC.KeepAlive(this)代替,但对于没有终结器的类,成本大致相同。 如果没有该调用,类包含引用的任何对象的终结器可能会在类自己的Dispose方法运行时触发。 这不是一个可能的情况,也不是即使发生通常也会导致问题的情况,但是将this传递给GC.KeepAlive(Object)GC.SuppressFinalize(Object)使这种古怪的情况变得不可能。

最新更新