我们已经讨论过,处理一个占用了非托管资源对象是很重要的。现在是时候 来讨论如何写代码来管理这些类占用的非内存资源了。一个标准的模式就是利用 .Net框架提供的方法处理非内存资源。你的用户也希望你遵守这个标准的模式。也就是通过实现IDisposable接口来释放非托管的资源,当然是在用户记得调用 它的时候,但如果用户忘记了,析构函数也会被动的执行。它是和垃圾回收器一 起工作的,确保在一些必要时候,你的对象只会受到因析构函数而造成的性能损 失。这正是管理非托管资源的好方法,因此有必要彻底的弄明白它。
处 在类继承关系中顶层的基类应该实现IDisposable接口来释放资源。这个类型也 应该添加一个析构函数,做为最后的被动机制。这两个方法都应该是用虚方法来 释放资源,这样可以让它的派生类重载这个函数来释放它们自己的资源。派生类 只有在它自己须要释放资源时才重载这个函数,并且一定要记得调用基类的方法 。
开始时,如果你的类使用了非内存资源,则一定得有一个析构函数。 你不能指望你的用户总是记得调用Dispose方法,否则当他们忘记时,你会丢失 一些资源。这或许是因为他们没有调用Dispose的错误,但你也有责任。唯一可 以确保非内存资源可以恰当释放的方法就是创建一个析构函数。所以,添加一个 析构函数吧!
当垃圾回收器运行时,它会直接从内存中移除不用析构的垃 圾对象。而其它有析构函数的对象还保留在内存中。这些对象被添加到一个析构 队列中,垃圾回收器会起动一个线程专门来析构这些对象。当析构线程完成它的 工作后,这些垃圾对象就可以从内存中移除了。就是说,须要析构的对象比不须 要析构的对象在内存中待的时间要长。但你没得选择。如果你是采用的这种被动 模式,当你的类型占用非托管资源时,你就必须写一个析构函数。但目前你还不 用担心性能问题,下一步就保证你的用户使用更加简单,而且可以避免因为析构 函数而造成的性能损失。
实现IDisposable接口是一个标准的模式来告诉 用户和进行时系统:你的对象占有资源而且必须及时的释放。IDisposable接口 只有一个方法:
public interface IDisposable
{
void Dispose( );
}
实现IDisposable.Dispose()方法有责 任完成下面的任务:
1、感知所有的非托管资源。
2、感知所有的 托管资源(包括卸载一些事件)。
3、设置一个安全的标记来标识对象已经 被处理。如果在已经处理过的对象上调用任何方法时,你可以检验这个标记并且 抛出一个ObjectDisposed的异常。
4、阻止析构。你要调用 GC.SuppressFinalize(this)来完成最后的工作。
通过实现IDisposable 接口,你写成了两件事:第一就是提供了一个机制来及时的释放所有占用的托管 资源(译注:这里就是指托管资源,当实现了这个接口后,可以通过调用Dispose 来立即释放托管资源),另一个就是你提供了一个标准的模式让用户来释放非托 管资源。这是十分重要的,当你在你的类型上实现了IDisposable接口以后,用 户就可以避免析构时的损失。你的类就成了.Net社区中表现相当良好的成员。
但在你创建的机制中还是存在一些漏洞。如何让一个派生类清理自己的 资源,同时还可以让基类很好的再做资源清理呢?(译注:因为调用Dispose方法 时,必须调用基类的Dispose,当然是在基类有这个方法时。但前面说过,我们 只有一个标记来标识对象是否处理过,不管先调用那个,总得有一个方法不能处 理这个标记,而这就存在隐患) 如果基类重载了析构函数,或者自己添加实现了 IDisposable接口,而这些方法又都是必须调用基类的方法的;否则,基类无法 恰当的释放资源。同样,析构和处理共享了一些相同的职责:几乎可以肯定你是 复制了析构方法和处理方法之间的代码。正如你会在原则26中学到的,重载接口 的方法根本没有如你所期望的那样工作。Dispose标准模式中的第三个方法,通 过一个受保护的辅助性虚函数,制造出它们的常规任务并且挂接到派生类来释放 资源。基类包含接口的核心代码, 派生类提供的Dispose()虚函数或者析构函数 来负责清理资源:
protected virtual void Dispose( bool isDisposing );
重载的方法同时完成析构和处理必须提供的任务 ,又因为它是虚函数,它为所有的派生类提供函数入口点。派生类可以重载这个 函数,提供恰当的实现来释放它自己的资源,并且调用基类的函数。当
isDisposing为true时你可能同时清理托管资源和非托管资源,当 isDisposing为false时你只能清理非托管资源。两种情况下,都可以调用基类的 Dispose(bool)方法让它去清理它自己的资源。
当你实现这样的模式时, 这里有一个简单的例子。MyResourceHog 类展示了IDisposable的实现,一个析 构函数,并且创建了一个虚的Dispose方法:
public class MyResourceHog : IDisposable
{
// Flag for already disposed
private bool _alreadyDisposed = false;
// finalizer:
// Call the virtual Dispose method.
~MyResourceHog()
{
Dispose( false );
}
// Implementation of IDisposable.
// Call the virtual Dispose method.
// Suppress Finalization.
public void Dispose()
{
Dispose( true );
GC.SuppressFinalize( true );
}
// Virtual Dispose method
protected virtual void Dispose( bool isDisposing )
{
// Don't dispose more than once.
if ( _alreadyDisposed )
return;
if ( isDisposing )
{
// TODO: free managed resources here.
}
// TODO: free unmanaged resources here.
// Set disposed flag:
_alreadyDisposed = true;
}
}
如果派生类有另外 的清理任务,就让它实现Dispose方法:
public class DerivedResourceHog : MyResourceHog
{
// Have its own disposed flag.
private bool _disposed = false;
protected override void Dispose( bool isDisposing )
{
// Don't dispose more than once.
if ( _disposed )
return;
if ( isDisposing )
{
// TODO: free managed resources here.
}
// TODO: free unmanaged resources here.
// Let the base class free its resources.
// Base class is responsible for calling
// GC.SuppressFinalize( )
base.Dispose( isDisposing );
// Set derived class disposed flag:
_disposed = true;
}
}
注和意,派生类和基类都有一个处理状态的标记, 这完全是被动的。重制的标记掩盖了在处理时任何可能发生的错误,而且是单一 的类型处理,而不是处理构成这个对象的所有类型。(译注:就是基类与子类各 自标记一个,互不影响。)
你应该被动的写处理方法和析构函数,处理对 象可能以任何顺序发生,你可能会遇到这种情况:你的类中某个成员在你调用 Dispose方法以前已经被处理过了。你没有看到这种情况是因为Dispose()方法是 可以多次调用的。如果在一个已经被处理过的对象上调用该方法,就什么也不发 生。析构函数也有同样的规则。任何对象的引用存在于内存中时,你不用检测 null引用。然而,你引用的对象可能已经处理掉了,或者它已经析构了。
这就引入用了一个非常重要的忠告:对于任何与处理和资源清理相关的 方法,你必须只释放资源! 不要在处理过程中添加其它任何的任务。你在处理和 清理中添加其它任务时,可能会在对象的生存期中遇到一些严重而繁杂的问题。 对象在你创建它时出生,在垃圾回收器认领它时死亡。你可以认为当你的程序不 能再访问它们时,它们是睡眠的。你无法访问对象,无法调用对象的方法。种种 迹象表明,它们就像是死的。但对象在宣布死亡前,析构函数还有最后一气。析 构函数什么也不应该做,就是清理非托管资源。如果析构函数通过某些方法让对 象又变得可访问,那么它就复活了。(译注:析构函数不是用户调用的,也不 由.Net系统调用,而是在由GC产生的额外线程上运行的) 它又活了,但这并不好 。即使是它是从睡眼中唤醒的。这里有一个明显的例子:
public class BadClass
{
// Store a reference to a global object:
private readonly ArrayList _finalizedList;
private string _msg;
public BadClass( ArrayList badList, string msg )
{
// cache the reference:
_finalizedList = badList;
_msg = (string)msg.Clone();
}
~BadClass()
{
// Add this object to the list.
// This object is reachable, no
// longer garbage. It's Back!
_finalizedList.Add( this );
}
}
当一个BadClass对象的析构函数执行时,它把自己的一 个引用添加到了全局的链表中。这使得它自己又是可达的,它就又活了。前面向 你介绍的这个方法会遇到一些让人畏缩的难题。对象已经被析构了,所以垃圾回 收器从此相信再也不用调用它的析构函数了。如果你实际要析构一个可达对象, 这将不会成功。其次,你的一些资源可能不再有用。GC不再从内存上移除那些只 被析构队列引用的对象,但它们可能已经析构了。如果是这样,它们很可能已经 不能使用了。(译注:也就是说利用上面的那个方法让对象复活后,很有可能对 象是不可用的。)尽管BadClass所拥有的成员还在内存里,它们像是可以被析构 或者处理,但C#语言没有一个方法可以让你控制析构的次序,你不能让这样的结 构可靠的运行。不要尝试。
我还没有看到这样的代码:用这样明显的方 式来复活一个对象,除非是学术上的练习。但我看过这样的代码,析构函数试图 完成一些实质的工作,最后还通过析构函数的调用把引用放到对象中,从而把自 己复活。析构函数里面的代码看上去是精心设计的,另外还有处理函数里的。再 检查一遍,这些代码是做了其它事情,而不是释放资源!这些行为会为你的应用 程序在后期的运行中产生很多BUG。删除这些方法,确保析构函数和Dispose()方 法除了清理资源外,什么也不做。
在托管环境里,你不用为每一个创建 的类写析构函数;只有须要释放一些使用的非托管资源时才添加,或者你的类所 包含的成员有实现了IDisposable接口的时候也要添加。即使如此,你也只用实 现IDisposable接口完成所有的功能就行了,不用析构函数。否则,你会限制你 的派生类实现实现标准的Dispose习惯。 遵守这个我所讲叙的标准的Dispose习 惯。这会让你的程序生活变得轻松,也为你的用户,也为那些从你的类创建派生 类的人。
本文转自cnn23711151CTO博客,原文链接: http://blog.51cto.com/cnn237111/554662,如需转载请自行联系原作者