.NET了解Dispose和Finalize方法么?

(1)背景知识

由于有了垃圾回收机制的支持,对象的析构(或释放)和C++有了很大的不同,这就需要我们在设计类型的时候,充分理解.NET的机制,明确怎样利用Dispose方法和Finalize方法来保证一个对象正确而高效地被析构。.

(2)Dispose方法

// 摘要:
//     定义一种释放分配的资源的方法。
[ComVisible(true)]
public interface IDisposable
{
    // 摘要:
    //     执行与释放或重置非托管资源相关的应用程序定义的任务。
    void Dispose();
}

Microsoft考虑到很多情况下程序员仍然希望在对象不再被使用时进行一些清理工作,所以.NET提供了IDispose接口并且在其中定义了Dispose方法。

通常我们会在Dispose方法中实现一些托管对象和非托管对象的释放以及业务逻辑的结束工作等等

But,即使我们实现了Dispose方法,也不能得到任何有关释放的保证,Dispose方法的调用依赖于类型的使用者,当类型被不恰当地使用,Dispose方法将不会被调用。因此,我们一般会借助using等语法来帮助Dispose方法被正确调用

(3)Finalize方法

刚刚提到Dispose方法的调用依赖于类型的使用者,为了弥补这一缺陷,.NET还提供了Finalize方法。Finalize方法类似于C++中的析构函数(方法),但又和C++的析构函数不同。Finalize在GC执行垃圾回收时被调用,其具体机制如下:

① 当每个包含Finalize方法的类型的实例对象被分配时,.NET会在一张特定的表结构中添加一个引用并且指向这个实例对象,暂且称该表为“带析构方法的对象表”;

② 当GC执行并且检测到一个不被使用的对象时,需要进一步检查“带析构方法的对象表”来查询该对象类型是否含有Finalize方法,如果没有则将该对象视为垃圾,如果存在则将该对象的引用移动到另外一张表,暂且称其为“待析构的对象表”,并且该对象实例仍然被视为在被使用。

③ CLR将有一个单独的线程负责处理“待析构的对象表”,其执行方法内部就是依次通过调用其中每个对象的Finalize方法,然后删除引用,这时托管堆中的对象实例就被视为不再被使用。

④ 下一个GC执行时,将释放已经被调用Finalize方法的那些对象实例。

上述四个步骤的完整流程如下图所示:

.NET了解Dispose和Finalize方法么?

(4)结合使用Dispose和Finalize方法:标准Dispose模式

Finalize方法由于有CLR保证调用,因此比Dispose方法更加安全(这里的安全是相对的,Dispose需要类型使用者的及时调用),但在性能方面Finalize方法却要差很多

因此,我们在类型设计时一般都会使用标准Dispose模式:Finalize方法作为Dispose方法的后备,只有在使用者没有调用Dispose方法的情况下,Finalize方法才被视为需要执行。这一模式保证了对象能够被高效和安全地释放,已经被广泛使用。

.NET了解Dispose和Finalize方法么?

下面的代码则是实现这种标准Dispose模式的一个模板:

public class BaseTemplate : IDisposable
{
    // 标记对象是否已经被释放
    private bool isDisposed = false;
    // Finalize方法
    ~BaseTemplate()
    {
        Dispose(false);
    }
    // 实现IDisposable接口的Dispose方法
    public void Dispose()
    {
        Dispose(true);
        // 告诉GC此对象的Finalize方法不再需要被调用
        GC.SuppressFinalize(this);
    }
    // 虚方法的Dispose方法做实际的析构工作
    protected virtual void Dispose(bool isDisposing)
    {
        // 当对象已经被析构,则不必再继续执行
        if(isDisposed)
        {
            return;
        }

        if(isDisposing)
        {
            // Step1:在这里释放托管资源
        }

        // Step2:在这里释放非托管资源

        // Step3:最后标记对象已被释放
        isDisposed = true;
    }

    public void MethodA()
    {
        if(isDisposed)
        {
            throw new ObjectDisposedException("对象已经释放");
        }

        // Put the logic code of MethodA
    }

    public void MethodB()
    {
        if (isDisposed)
        {
            throw new ObjectDisposedException("对象已经释放");
        }

        // Put the logic code of MethodB
    }
}

public sealed class SubTemplate : BaseTemplate
{
    // 标记子类对象是否已经被释放
    private bool disposed = false;

    protected override void Dispose(bool isDisposing)
    {
        // 验证是否已被释放,确保只被释放一次
        if(disposed)
        {
            return;
        }

        if(isDisposing)
        {
            // Step1:在这里释放托管的并且在这个子类型中申明的资源
        }

        // Step2:在这里释放非托管的并且这个子类型中申明的资源

        // Step3:调用父类的Dispose方法来释放父类中的资源
        base.Dispose(isDisposing);
        // Step4:设置子类的释放标识
        disposed = true;
    }
}

真正做释放工作的只是受保护的虚方法Dispose,它接收一个bool参数,主要用于区分调用者是类型的使用者还是.NET的GC机制。

两者的区别在于通过Finalize方法释放资源时不能再释放或使用对象中的托管资源,这是因为这时的对象已经处于不被使用的状态,很有可能其中的托管资源已经被释放掉了

在Dispose方法中GC.SuppressFinalize(this)告诉GC此对象在被回收时不需要调用Finalize方法,这一句是改善性能的关键,记住实现Dispose方法的本质目的就在于避免所有释放工作在Finalize方法中进行