上篇文章中我提到,CLR通过栈指针给变量分配内存空间,通过GC来释放不再引用的内存空间。GC虽然减少了程序员处理内存的困难,但它也有局限性,它不能处理像文件句柄、网络连接、数据库连接这样的非托管资源。在定义一个类时,我们使用两种机制来自动释放非托管资源:
1,声明一个析构函数(或终结器finalizer),作为类的一个成员
2,在类中实现System.IDisposable接口
析构函数:
析构函数的声明格式如下
class Hotel { ~Hotel() { //destructor implementation } }
在GC销毁对象之前,会调用对象的析构函数。C#编译器在编译析构函数时,会隐式地把析构函数的代码编译为等价于Finalize()方法的代码,从而确保执行父类的Finalize()方法。下面模拟了编译器编译析构函数生成的c#代码:
protected override void Finalize() { try { //destructor implementation } finally { base.Finalize(); } }
如上所示,在~Hotel()析构函数中的实现代码封装在Finalize()方法的一个try语句块中。对父类的Finalize()方法的调用放在finally块中,确保该调用的执行。
有经验的C++开发人员会大量使用析构函数,用于清理资源,提供调试信息或执行一些其他的操作。这是因为,C++中,销毁对象时,它的析构函数会立即执行。但是,C#中,我们使用析构函数的次数却少之又少。C#析构函数的问题在于它的不确定性,由于GC的工作方式,我们无法确定C#析构函数何时会执行。因此,我们不敢在析构函数中放置一些需要在特定时刻执行的代码。对象如果占用了宝贵而重要的资源,应该尽快释放这些资源,此时就不能指望GC来自动清理资源了。另一个问题是,C#析构函数的实现会延迟对象从内存中删除的时间。没有析构函数的对象会在GC的一次处理中从内存中删除。但有析构函数的对象却需要两次的处理才能销毁。第一次调用不会删除,第二次调用时才会执行删除操作。另外,CLR使用一个线程来执行所有对象的Finalize()方法,如果频繁使用析构函数,并让它们执行长时间的清理任务,对性能的影响就会非常显著。
实现IDisposable接口:
在C#中,推荐使用System.IDisposable接口代替析构函数。IDisposable接口为释放非托管资源提供了一种确定的机制,并避免产生析构函数固有的与GC相关的问题。IDisposable接口的实现如下:
class Hotel: IDisposable { public void Dispose() { //implementation } }
Dispose()方法的实现代码显式地释放由对象直接使用的非托管资源,并在所有也实现IDisposable接口的封装对象上调用Dispose()方法。这样,Dispose()方法就为释放非托管资源提供了一种精确的控制。
看下面的代码:
Hotel hotel = new Hotel(); hotel.Dispose();
该代码会调用hotel对象的Dispose()方法,但是如果执行过程出现异常,这段代码就不会释放对象使用的资源。所以,我们对它加以改进:
Hotel hotel = null; try { hotel = new Hotel(); } finally { if (hotel != null) { hotel.Dispose(); } }
这样就能确保,即使执行过程出现异常,也总能保证在hotel上调用Dispose()方法。但是如果我们的项目中重复使用这样的结构,代码就会变得非常杂乱。C#提供了一种语法,使用using关键字,生成与上面等同的代码:
using (Hotel hotel = new Hotel()) { //operation }
是不是很熟悉,我们在使用数据库连接和网络连接时经常用到这种写法。它也可以保证在出现异常时调用Dispose()方法。
在实际应用中,我们需要综合使用以上两种方法来进行非托管资源的释放工作:
class
Hotel: IDisposable
{
private
bool
isDisposed =
false
;
~Hotel()
{
Dispose(
false
);
}
public
void
Dispose()
{
Dispose(
true
);
GC.SuppressFinalize(
this
);
}
protected
virtual
void
Dispose(
bool
disposting)
{
if
(!isDisposed)
{
if
(disposting)
{
//Cleanup managed objects by calling their Dispose()
}
//Cleanup unmanaged objects
}
isDisposed =
true
;
}
public
void
Operate()
{
if
(isDisposed)
{
throw
new
ObjectDisposedException(
"Hotel"
);
}
//implementation
}
}
|
在该示例中,Dispose(bool disposting)方法执行真正的清理工作。它接受一个参数disposting,用来标示调用是来自Dispose()方法还是析构函数。在调用Dispose()方法时,我们用到了GC.SuppressFinalize(this);它会告诉GC,在调用当前对象的Dispose()方法时,不再执行对象的析构函数。在Operate()方法中,会根据变量isDisposed来确定对象是否已经被销毁,如果被销毁,会抛出一个ObjectDisposedException类型的异常。