Effective C# Item18: Implement the Standard Dispose Pattern

      释放非托管资源是非常重要的一个环节。对于我们自定义的类型来说,如果需要释放资源,那么我们应当使用.Net Framework的标准模式来释放非托管资源。标准模式使用IDisposable接口来显式释放资源,而当用户忘记释放时使用终结器(finalizer)来进行释放。是否应当调用对象的终结器是通过GC(Garbage Collector)来确定的。这是处理非托管资源的正确方法。

      我们自定义类型的基类型应当实现IDisposable接口以便释放资源,同时我们也应当添加保护性的终结器。当我们的派生类中有其独有的需要释放的资源时,我们可以重写它们来恰当的释放资源。

      首先,如果我们的类使用了非托管资源,那么必须要有终结器。我们不能指望用户程序每次都调用Dispose()方法。虽然造成资源浪费是他们没有调用Dispose的错误,但是我们可以通过创建终结器来解决这种问题。

      当GC开始工作的时候,它首先将没有终结器的垃圾对象从内存中移除,有终结器的所有对象则添加到一个垃圾队列当中。GC会调用一个新县城来执行这些对象的终结器。当终结器执行完毕后,这个对象会从队列中被移除。有终结器的对象会比没有的在内存中保留更长的时间。这虽然不是我们希望的,但是为了提高防护性,我们必须为使用非托管资源的类型提供终结器。但是我们不必担心这样会降低性能,因为下一步我们可以轻松的避免这些消耗。

      通知用户和系统释放资源的标准方法是实现IDisposable接口。这个接口只包含一个方法:

public interface IDisposable
{
      
void Dispose();
}

      当我们实现这个方法的时候,应当做四项工作:

      1. 释放所有非托管资源

      2. 释放所有托管资源(包括未解开的事件)

      3. 设置标记来记录该对象是否被释放。我们应当在释放资源前检查设置。如果对已经释放过资源的对象调用Dispose,应当抛出ObjectDisposed异常。

      4. 禁止终结器。调用GC.SuppressFinalize(this)方法来禁止终结器对这个对象的操作。

      通过实现IDisposable接口我们完成了两项工作:为客户程序提供了及时释放托管资源的机制和释放非托管资源的标准方法。这是非常重要的。通过它客户端可以避免调用终结器的资源消耗。

      但是我们的机制还有其他问题。如果使得派生类在释放资源的时候让基类也释放其资源?如果派生类重写了finalize方法或者实现了自己的IDisposable接口,那么这些方法必须调用基类,否则就不能恰当的释放基类的资源。同时,finalize和Dispose有一些相同的职责,一般来说它们的代码大部分都是相同的。在标准的释放资源模式中还有第三种方法,这是一个受保护的虚拟辅助函数。它通过为派生类添加释放资源的钩子来达到整合上述工作的任务。在基类中包含了核心接口的代码,这个虚拟方法为派生类提供了清理资源的钩子来响应Dispose或者终结器:

protected virtual void Dispose(bool isDisposing)

      这个重载的方法支持终结器和Dispose的必要方法,而且由于它是虚拟的,还能为派生类提供入口点。派生类可以重写这个方法来恰当的实现对资源的释放,并调用其基类的方法来释放基类资源。当isDisposing为true的时候,我们清理托管和非托管资源。当其为false时我们之清理非托管资源。在这之后调用基类的Dispoe(bool)来清理基类资源。

下面是个简单的例子来实现这种资源释放模式。MyResourceHog类实现了Idispose接口,终结器,并创建了一个虚方法Dispose。

        public class MyResourceHog: IDisposable
        
{
            
private bool _alreadyDisposed = false;

            
//finalizer
            ~MyResourceHog()
            
{
                Dispose(
false);
            }


            
//virtual Dispose
            protected virtual void Dispose(bool isDisposing)
            
{
                
if(_alreadyDisposed)
                
{
                    
return;
                }

                
if(isDisposing)
                
{
                    
//释放托管资源
                }

                
//释放非托管资源
                _alreadyDisposed = true;
            }


            
IDisposable 成员

        }

      如果派生类有额外的资源需要释放,则重写受保护的Dispose方法: 

        public class DerivedResourceHog: MyResourceHog
        
{
            
private bool _disposed = false;

            
protected override void Dispose(bool isDisposing)
            
{
                
if(_disposed)
                
{
                    
return;
                }

                
if(isDisposing)
                
{
                    
//释放托管资源
                }

                
//释放非托管资源
                base.Dispose(isDisposing);
                _disposed 
= true;
            }

        }

      应当注意的是不论是基类还是派生类都有一个成员变量来标记该对象其是否已被释放。这是防护性的考虑。通过这个标记可以定位异常到某一个确定的类,而不是对象中所包含的所有类。

      我们需要防护性的编写Dispose和终结器。释放资源的时候释放的顺序可能是任意的。在有些情况下我们还尚未调用Dispose,但对象就已经被清理掉了。我们不应当将其视为重复调用Dispose的错误。当一个对象已经被dispose过了之后,再次的调用将不做任何操作。终结器也遵循同样的规则。此时我们的对象都还存在于内存中,我们不必检查它们是否为null。但是它们可能已经被释放了,也可能已经被终结了。

      这为我们在对象清理时提出了一个重要的建议:我们要做的只是释放资源。不要在这些方法中进行其他的操作,这样会为我们带来严重的问题。对象应当在创建时“出生”,在GC收回资源后“死亡”。对于这些被释放资源的对象,它们的状态类似于“昏睡”。我们应当将这些对象视为不可达的。如果一个对象是不可达的,我们也就不能调用其任何方法。在这个意义上,它是“死亡”的。但是对象在调用其终结器后其实还留有一口气。终结器不应当做清理资源以外其他的操作。如果一个终结器的某些操作使一个终结对象重新可达了,我们称其“复活”(resurrected)。下面是一个例子:

        public class BadClass
        
{
            
private readonly ArrayList _finalizedList;
            
private string _msg;
            
public BadClass(ArrayList badList, string msg)
            
{
                _finalizedList 
= badList;
                _msg 
= (string)msg.Clone();
            }


            
~BadClass()
            
{
                
//通过List使得被释放对象可达了
                _finalizedList.Add(this);
            }

        }

      当BadClass对象调用终结器时,它将本身的引用放到了一个全局表中。这就使得它是可达的了,它复活了。这样做带来的问题会让所有人畏惧。对象已经被终结了,因此GC认为已经不必要再次调用它的终结器了。如果我们需要再次终结一个复活对象的时候,它是不会发生的。另外,我们的一些资源变得不可用了。GC不会将只有终结队列中的对象可达的对象从内存中移除,但是它们可能已经被终结了。如果这样的化,它们就一定不能再使用了。尽管BadClass的成员还在内存中,但是它们就好像是已经被释放掉了一样。在C#中我们不能控制终结的顺序。我们不能使它更加可靠。不要尝试这种做法。

      我还没有见到过这样明显的使用复活对象的代码,除了在校的练习之外。但是有一些代码在终结器中尝试着做一些确实的工作,当某些保存了其本身引用的函数被调用时复活该对象。理论上我们应当对终结器和Dispose非常小心。如果真的需要做一些其他的工作,我们应当认真注意这些动作是否会带来问题。尽量不要在其中做任何与释放资源无关的操作。

      在托管环境中,我们不需要为每个自定义的类型都创建终结器。我们只为那些储存非托管资源或者包含实现了IDisposable接口的成员的类型释放资源。即便我们只需要Disposable接口部分,而不需要终结器,我们也应当实现整个模式。

      译自   Effective C#:50 Specific Ways to Improve Your C#                      Bill Wagner著

      回到目录

posted on 2006-10-27 10:48  aiya  阅读(1811)  评论(0编辑  收藏  举报