1.前言
对.NET/C#稍有了解的同学,都应该知道IDispose模式的存在,但不知道有多少同学能彻彻底底地理解这种模式。楼主本人初识IDispose模式也有很长时间了,但对其设计原理和初衷也一直是云里雾里。直到这两天终于下定决心想彻底理解其工作模式,上网翻阅了不少资料,才算有所领悟,特别是StackOverFlow上的这篇文章:https://stackoverflow.com/questions/538060/proper-use-of-the-idisposable-interface/538238#538238,让我有茅舍顿开之感,极力推荐各位英文不错的同学参看此文。
2.你是否也有这些疑问:
什么情况下需要实现IDisposeable接口?
IDisposable模式与垃圾收集器(GC)之间到底有什么关联?
isDisposing标志什么时候应该置为true,什么时候应该置为false,各自的意义是什么?
如果你也有如上疑问,希望看完本文,能给你一个满意的答复。
3. 托管资源和非托管资源
我们知道,在.NET平台下,代码中的资源类型包含两种:托管资源和非托管资源。你在.NET平台下创建的对象,以及你从FCL类库中调用的类型,一般都属于托管资源。只有windows窗体句柄、数据库连接以及网络连接(Socket)之类,以及你利用P/Invoke调用的Windows API属于非托管资源。
4. 非托管资源释放
如果你在一个(C#)类中创建了一个非托管资源,那么垃圾回收器将无法对其进行自动回收,这时候,程序员需要负责完成对该非托管资源的清理工作。作为类设计者,有多种方式可完成该项清理工作,比较常用的方法是,提供一个形如Clear()之类的清理接口函数,供类使用者调用,类设计者负责在该函数中完成清理工作,例如:
-
namespace
Sample
-
{
-
public
class
Sample
-
{
-
....
-
private intptr _handle;
-
-
public void Clear()
-
{
-
CloseHandle(_handle);
-
}
-
}
-
}
只要类使用者对该函数设计意图的理解没有偏差,在适当时机调用该函数以完成清理工作,那么这种方式就能实现非托管资源的释放。但是,.NET为我们设计了一种更好的模式 -- IDisposable接口:
public interface IDisposable { void Dispose(); }
有了IDisposable接口后,可按如下方式修改Sample类,使其更便于调用者使用(而不必去猜测Clear()函数的用意):
-
namespace
Sample
-
{
-
public
class
Sample:
IDisposable
-
{
-
....
-
private intptr _handle;
-
-
public void Dispose()
-
{
-
CloseHandle(_handle);
-
}
-
}
-
}
这样,类使用者只要调用Dispose()函数,就可以完成类对象中非托管资源的清理。
5. Finalize()函数
我们通过约定(IDisposable接口)实现了非托管资源的清理工作。但这只是约定,其最终是否能达成,取决于类使用者。若使用者忘记了调用该接口,那么前面所做的工作都将成为徒劳,对象中的非托管资源仍将存在,并在该对象被销毁后处于无政府状态,这对于程序来说,是不可接受的。
因此,类设计者应采用某种机制,保证Dispose()函数一定被调用。这时,稍有经验的设计者,一般会想到C#类的Finalize()函数,该函数在对象被销毁时由垃圾收集器GC调用,用于清理对象中的资源,这种机制类似于C++的析构函数。在Finalize()函数中调用Dispose()函数,可保证非托管资源一定会在某个时机被清除。此时,样例类将变成如下样式:
注:C#中不可直接重写Finalize()函数,而应以类似于C++析构函数的方式实现,此处~Sample()等同于Finalize()函数
namespace Sample { public class Sample { .... private intptr _handle; public void Dispose() { CloseHandle(_handle); } ~Sample() { Dispose(); } } }
6. 清理托管资源
截止目前,我们基本实现(还有一点小问题,下文详述)了对象中非托管资源的清理。那么,对于托管资源我们是否应该一味得交由垃圾收集器来自动清理呢?假如,一个对象中托管资源占用了大量的内存资源,而垃圾收集器可能需要一段时间后才会光顾该对象,在这期间,我们就应该撒手不管,任由宝贵的内存资源被肆意侵占吗?我想这不是一个有良好职业操守的程序员愿意看到的。因此对于系统资源消耗较多或侵占关键资源的托管对象,我们也应该像处理非托管资源一样,对其进行手动清理,当然这只是建议,并非必须如此。
最直接清理托管资源的方法,就是在Dispose()函数中,加入对其进行的清理操作,如:
7. isDisposing标志
namespace Sample { public class Sample { ..... private intptr _handle; private object _managedObj; public void Dispose() { CloseHandle(_handle); if(_managedObj != null) _managedObj = nu;//之后GC将会自动将该对象销毁 } } }
上述对托管资源的清理操作,可能引发问题。如果Dispose()方法是由垃圾收集器(GC)在通过Finalize()函数调用,那么,这时候GC已经销毁了该对象中的托管资源,此时再调用_managedObj.Dispose()可能引发异常(虽然之前已经做了null判断,但仍有此可能),即使不引发任何异常,这种重复操作也是多此一举。
因此,我们需要一个标志位来判别当前是谁在调用Dispose()函数,这个标志位就是isDisposing, 当其为True时,说明是调用者通过IDisposable接口在调用该函数;当前为false时(相当于isFinalizing),说明当前是由Finalize()函数在调用该函数,也即是由GC在调用。
加入isDisposing标志后,Dispose函数应形如:void Dispose(bool isDisposing);但此签名与IDisposable接口定义相违,所以我们定义一个第三方函数,供IDisposable接口和Finalize()函数调用。约定俗成,这个第三方函数仍旧命名为Dispose(),只是其访问属性和签名有所区别,加入该函数后,IDisposable模式将形如:
-
namespace
Sample
-
{
-
public
class
Sample
-
{
-
....
-
private intptr _handle;
-
private
object _managedObj;
-
-
public void Dispose()
-
{
-
Dispose(
true);
//调用第三方函数
-
}
-
-
protect virtual void Dispose(bool isDisposing)
-
{
-
CloseHandle(_handle);
-
-
if(isDisposing)
-
{
-
if(_managedObj !=
null)
-
_managedObj =
null;
-
}
-
}
-
-
~Sample()
-
{
-
Dispose(
false);
-
}
-
}
-
}
8. GC.SuppressFinalize()
经过一系列优化后,上述样例代码已趋于完善,但仍有一个比较大的问题:若类使用者在某个时机调用Dispose()函数,销毁了对象中的非托管资源,而当该对象不再被引用后,垃圾收集器GC将在某个时机调用Finalize()函数,即再次调用了Dispose()函数,但此时非托管资源已不存在,再次对其进行清理将引发bug。
对于以上问题,.NET的垃圾收集机制提供了SuppressFinalize()函数,调用该函数相当于通知垃圾收集器不要再调用该对象的Finalize函数。因此,我们应在对外接口函数Dispose()中,加入GC.SuppressFinalize(),加入后形如:
-
namespace
Sample
-
{
-
public
class
Sample
-
{
-
....
-
private intptr _handle;
-
private
object _managedObj;
-
-
public void Dispose()
-
{
-
Dispose(True);
-
GC.SuppressFinalize();
-
}
-
-
protect virtual void Dispose(bool isDisposing)
-
{
-
CloseHandle(_handle);
-
-
if(isDisposing)
-
{
-
if(_managedObj !=
null)
-
_managedObj =
null;
-
}
-
}
-
-
~Sample()
-
{
-
Dispose(
false);
-
}
-
}
-
}
9. isDisposed标志
最后,一般为了防止重复清理,会在IDisposable模式加入isDisposed标志。处理方式如下:
-
namespace
Sample
-
{
-
public
class
Sample
-
{
-
....
-
private intptr _handle;
-
private
object _managedObj;
-
private
bool _isDisposed;
-
-
public void Dispose()
-
{
-
Dispose(
true);
-
GC.SuppressFinalize();
-
}
-
-
protect virtual void Dispose(bool isDisposing)
-
{
-
if(!_isDisposed)
-
{
-
CloseHandle(_handle);
-
-
if(isDisposing)
-
{
-
if(_managedObj !=
null)
-
_managedObj =
null;
-
-
}
-
_isDisposed =
true;
-
}
-
-
~Sample
-
{
-
Dispose(
false);
-
}
-
}
-
}
-
-
}
10.总结
最后,对一开始提出的问题进行一下回答:
什么情况下需要实现IDisposable接口:类中存在非托管资源时,就应该实现该接口;类中存在消耗系统资源较多的托管对象时,建议实现该接口。
IDisposable模式与GC之间有什么联系:二者并无实际联系,但在Dispose接口函数中应调用GC.SuppressFinalize()函数通知GC不要再次清理
isDisposing标志什么时候应该置为true,什么时候应该置为false:通过Dispose接口函数调用时,应为True,否则应为false。