如何实现标准的dispose
前面的文章我们说过,如果对象包含非托管资源那么就必须要正确的清理,现在我们就来说一下如何清理。针对非托管资源 .NET 会采用一套标准的模式来完成清理工作。也就是说如果开发人员自己编写的类中存在非托管资源,那么这个类的使用者就会认为这个类遵循 .NET 的垃圾清理模式。标准的 dispose 模式即实现了 IDisposable 接口,又实现了 finalizer ,这样就可以在客户端忘记调用 IDisposable.Dispose 的情况下也可以释放资源。
Tip:在 .NET 中访问非托管资源还可以通过 System.Runtime.Interop.SafeHandle 的派生类来访问,该类正确实现了标准的 dispose 。
零、基类与子类需要注意
在详细讲解具体如何正确实现 dispose 模式前我们要了解基类与子类需要注意的内容。
- 基类
- 实现 IDisposable 接口,以便释放资源;
- 如果类本身包含非托管资源,那么就需要添加 finalizer 来防止客户端忘记调用 Dispose 方法;
- Dispose 方法和 finalizer 资源释放必须交给虚方法,这样子类才可以重写释放资源的方法。
- 子类
- 假如子类需要自己释放资源,那就必须重写基类所定义的释放资源的虚方法,如果基类不存在这个虚方法那就不需要重写;
- 假如子类中存在使用非托管资源的情况,那就必须实现 finalizer ;
- 重写基类释放资源的函数时,一定要调用基类的同名函数。
一、详解
当我们编写的类中存在必须释放的资源的时候,我们就必须实现 IDisposable 接口,这个接口只包含一个无返回值的无参 Dispose 方法。在实现该方法时又如下几个方面需要注意的:
- 释放所有不再使用的非托管资源;
- 释放所有不再使用的托管资源;
- 设置状态标志,表示对象已被清理过,如果有代码调用被清理过的对象那么就可以通过这个标志得知,进而手动抛出 ObjectDisposedException 异常;
- 通过 GC.SupperssFinalize(this) 来阻止垃圾回收器重复清理已被清理过的对象。
虽然实现 Dispose 方法在保证释放托管资源和非托管资源的情况下又能保证程序性能不会下降,但是它依然存在问题。子类在清理自身资源的同时还必须保证基类资源也被清理掉。这时大部分开发人员能想到的解决方法就是重写 finalizer并调用基类释放资源的方法,或者给 Dispos 方法添加新的逻辑并调用基类释放资源的方法。这两种方法都有类似的任务需要完成,因此这两种方法包含了大量重复的代码,这时我们就需要将这两种方法中重复的代码提取到一个 protected 级别的虚函数种,这样基类只需写好核心逻辑,子类重写这个方法用来释放自己的资源就可以了。一般我们会将这个方法定义成如下的样子:
protected virtual void Dispose(bool isDisposing)
在上述代码中如果 isDisposing 为 true,则代表托管资源和非托管资源都要清理,如果为 false 则代表只清理非托管资源。不管是 true 还是 false 我们都需要在子类中调用基类的 Dispose(bool) 方法。下面我们通过一段代码来看一下我们该怎么实现上述所说的内容。
public class DemoBase:IDisposable
{
private bool baseDisposed = false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool isDisposing)
{
if(baseDisposed)
{
return;
}
if(isDisposing)
{
//清理托管资源
}
//清理非托管资源
baseDisposed=true;
}
public void Method()
{
if(baseDisposed)
{
throw new ObjectDisposedException("Demo","资源已被释放!");
}
//more code
}
}
public class Demo:DemoBase
{
private bool disposed = false;
protected override void Dispose(bool isDisposing)
{
if(baseDisposed)
{
return;
}
if(isDisposing)
{
//清理托管资源
}
//清理非托管资源
base.Dispose(isDisposing);
disposed=true;
}
//more code
}
上述代码中 Demo 类继承自 DemoBase,并重写了 Dispose 方法。我们在代码中看到基类和子类都使用了标志状态(baseDisposed,disposed),这里为什么不使用同一个标志状态呢?主要是因为子类在释放资源的时候有可能会把标志状态改为 true,这时在运行基类的 Dispose(bool) 方法基类会认为资源已经释放过了。
Tip:Dispose(bool) 和 finalizer 都必须编写的很可靠,同时具备幂等性。也就是说无论多少次调用 Dispose(bool) 的效果都是一样的。并且我们在释放资源的时候我们不应该进行除了释放资源以外的操作。