C#中的Dispose模式
声明
本文中的内容属于个人总结整理而来,个人水平有限,对于部分细节难免有理解错误及遗漏之处,如果您在阅读过程中有所发现,希望您能指正,同时文章中的部分内容也参考了其它大神的文章,如果文章中的内容侵犯了您的权益,表示非常歉意,请您指出,我将尽快修改。
如果文章中的内容侵犯了您的权益,表示非常歉意,请您指出,我将尽快修改。
如果您进行转载,请标明出处。
C#中的Dispose模式(http://www.liyubin.com/articles/2020/11/12/1605171494657.html)
简介
在C#中的每一个类型都可以看做是一种资源,这些资源可以分为两类:
- 托管资源:由CLR管理分配和释放的资源,即从CLR里new出来的对象。
- 非托管资源:不受CLR管理的对象,如Windows的窗口,文件句柄、数据库连接、套接字、COM对象等。
对于托管资源来说,由于受到CLR的管理,我们不需要关心资源释放的问题,完全可以依赖于垃圾回收器来进行内存的管理,但是对于非托管资源来说,由于其不受CLR管理,使用完毕后,就需要显式释放这些资源。
对于非托管资源,则应该执行以下操作:
-
实现Dispose模式
这要求你提供 IDisposable.Dispose 实现以启用非托管资源释放,当不再需要此对象(或其使用的资源)时,调用者可以通过调用Dispose方法释放非托管资源
-
调用者忘记调用Dispose方法的情况下,需要提供一种方法来释放非托管多资源
- 使用安全句柄包装非托管资源
- 重写 Object.Finalize 方法
本方将重点总结一下Dispose模式的实现方式及注意事项,同时会结合《编写高质量代码改善C#程序的157个建议》一书中的内容进行一下总结。
IDisposable接口
C#提供了标准的接口IDisposable,通过实现接口中的Dispose方法来实现非托管资源的释放。
public interface IDisposable
{
void Dispose();
}
通过实现IDisposable接口中的Dispose方法,可以显式的释放非托管资源,同时也在提醒调用者此类型的资源需要通过Dispose方法的调用来显式释放资源
标准的Dispose模式的实现
using System;
public class BaseClass : IDisposable
{
// 添加标识,用于标识此资源是否已经回收过了
private bool _disposed = false;
//防止调用者未显式调用Dispose方法
~BaseClass()
{
Dispose(false);
}
// 实现IDisposable中的Dispose方法
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
// TODO: 释放托管资源
}
// TODO: 释放非托管资源
_disposed = true;
}
public void PMethod()
{
if(__disposed)
{
throw new ObjectDisposedException("it has been disposed");
}else
{
...
}
}
}
标准的Dispose模式剖析
以下内容主要来自《编写高质量代码改善C#程序的157个建议》一书
-
如果类型需要显式的释放资源,那么一定要继承IDispose接口
在.NET中每次使用new操作符创建对象时,CLR都会为该对象在堆上分配内存。对于没有继承IDisposable接口的类型对象,垃圾回收器则会直接释放对象所占用的内存:而对于实现了Dispose模式的类型,每次创建对象的时候,CLR都会将该对象的一个指针放到终结列表中,垃圾回收器在回收该对象的内存前,首先将终结列表中的指针放到一个freachable队列中。同时,CLR还会分配专门的线程读取freachable队列,并调用对象的终结器,只有这个时候对象才会真正被识别为垃圾,并且在下一次进行垃圾回收时释放对象所占的内存。
除此外继承IDispose接口为实现Using语法糖提供了便利,编译器会自动生成IDispose方法的IL代码:
using(BaseClass bClass = new BaseClass()) { }
某种意义上来说上述的代码段相当于:
BaseClass bClass; try { bClass = new BaseClass(); }finally{ bClass.Dispose(); }
-
即使提供了显式的释放方法,也应该在终结器中提供隐式清理
在标准的Dispose模式中,可以看到终结器(析构函数)的实现:
~BaseClass() { Dispose(false); }
为当前类型提供终结器的意义在于,我们不能奢望类型的调用者肯定会主动调用Dispose方法,基于终终结器(析构函数)会被垃圾回收器调用这个特点下,它将用作资源释放的补救措施。
对于实现了Dispose模式的类型对象,起码要经过两次垃圾回收才能真正地被回收掉,因为垃圾回收机制会安排CLR调用终结器。基于这个特点,如果我们的类型提供了显式释放的方法来减少一次垃圾回收,同时也可以在终结器中提供隐式清理,以避免调用者忘记调用该方法而带来的资源泄漏。
如果调用者已经调用Dispose方法进行了显式地资源释放,那么,隐式资源释放(就是终结器)就没有必要再运行了。FCL中的类型GC提供了静态方法SuppressFinalize来通知垃圾回收器这一点。
public void Dispose() { Dispose(true);//进行正常的资源回收 GC.SuppressFinalize(this);//通知垃圾回收器不再需要调用终结器(析构函数) }
-
Dispose方法应允许被多次调用
在类型的内部通过维护一下私有的变量_disposed,来标识对象是否被释放过,以此来保证Dispose方法被多次调用而不抛出异常。
private bool _disposed = false;
在实际清理代码的方法中,加入一下判断,如果类型已经被清理过一次则清理工作将不再进行
protected virtual void Dispose(bool disposing) { if (_disposed)//如果已经被清理过,将不再执行 { return; } ...... }
需要注意的是:对象被调用过Dispose方法,并不表示该对象已经被垃圾回收器回收内存,被置为null,已经不存在了。事实上该对象的引用可能还在,由于对象被Dispose过,部分资源已经被释放,对象当前状态已经不正常了,那些在这种情况下如果调用对象的公开的方法,应该会抛出一个异常。
public void PMethod() { if(__disposed) { throw new ObjectDisposedException("it has been disposed"); }else { ... } }
-
在Dispose模式中应该提取一个受保护的虚方法
在标准的Dispose模式的实现可以看到IDisposable接口的Dispose方法并没有做实际的清理工作,它其实是调用了带bool参数且受保护的的虚方法:
protected virtual void Dispose(bool disposing) { ... }
之所以提供这样一个受保护的虚方法,是因为考虑了这个类型会被其他类型继承的情况。如果类型存在一个子类,子类也许会实现自己的Dispose模式。受保护的虚方法用来提醒子类:必须在自己的清理方法时注意到父类的清理工作,即子类需要在自己的释放方法中调用base.Dispose方法。
using Microsoft.Win32.SafeHandles; using System; using System.Runtime.InteropServices; class DerivedClass : BaseClass { // To detect redundant calls private bool _disposed = false; // Instantiate a SafeHandle instance. private SafeHandle _safeHandle = new SafeFileHandle(IntPtr.Zero, true); // Protected implementation of Dispose pattern. protected override void Dispose(bool disposing) { if (_disposed) { return; } if (disposing) { // Dispose managed state (managed objects). _safeHandle?.Dispose(); } _disposed = true; // 调用父类的Dispose base.Dispose(disposing); } }
如果不为类提供这个受保护的虚方法,很有可能让开发者设计子类的时候忽略掉父类的清理工作。所以要在类型的Dispose模式中提供一个受保护的虚方法
-
在Dispose模式中应该区别对待托管资源和非托管资源
真正资源释放代码的那个虚方法是带一个bool参数的,带这个参数,是因为我们在资源释放时要区别对待托管资源和非托管资源。
public void Dispose() { //必须为true Dispose(true); ... }
这时候代码要同时处理托管资源和非托管资源。
在终结器(析构函数)中调用的是false
~BaseClass() { //必须为false Dispose(false); }
这表明隐式清理时,只要处理非托管资源就可以了。
为什么要区别对待托管资源和非托管资源呢?
在这个问题前,我们首先要弄明白:托管资源需要手工清理吗?不妨将C#中的类型分为两类,一类继承了IDisposable接口,一类则没有继承。前者,暂时称为非普通类型,后者称为普通类型。非普通类型因为包含非托管资源,所以它需要继承IDisposable接口,但是,这里包含非托管资源的类型本身,它是一个托管资源。所以,托管资源中的普通类型不需要手动清理,而非普通类型是需要手工清理的(即调用Dispose方法)。
Dispose模式设计的思路是:如果调用者显式调用了Dispose方法,那么类型就应该按部就班地将自己的资源全部释放。如果调用者忘记调用Dispose方法,那么类型就假设自己的所有托管资源(哪怕是那些非普通类型)会全部都交给垃圾回收器回收,所以不进行手工清理。所以在Dispose方法中,虚方法传入参数true,在终结器中,虚方法传入参数false。
参考资料
- 清理未托管资源
- 编写高质量代码改善C#程序的157个建议(建议46-50)
- IDisposable 接口