.Net关于对象的销毁(IDisposable和using) - 非托管资源

来源:互联网 发布:php有哪些优势和特点 编辑:程序博客网 时间:2024/04/29 18:03

        CLR 有一个 垃圾收集GC  机制,可以管理内存分配和回收等工作,在绝大多数情况下,程序员只需要new 一个对象,而将销毁这一对象的工作完全交给CLR代劳。
        但是,我们所编写的类中使用了非托管的资源,比如文件句柄,用于线程同步的Mutex对象,或者是数据库连接,这些资源应该遵循“即需即建即销毁”的原则,

        这就是说:需要的时候才创建这些对象,用完之后就马上销毁。

        析构函数(destructor) 与构造函数相反,当对象脱离其作用域时(例如对象所在的函数已调用完毕),系统自动执行析构函数。析构函数往往用来做“清理善后” 的工作(例如在建立对象时用new开辟了一片内存空间,应在退出前在析构函数中用delete释放)。


        以C++语言为例,析构函数名也应与类名相同,只是在函数名前面加一个波浪符~,例如~stud( ),以区别于构造函数。它不能带任何参数,也没有返回值(包括void类型)。只能有一个析构函数,不能重载。如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数,它也不进行任何操作。所以许多简单的类中没有用显式的析构函数。

 

解构器

     我们知道,“解构器”被用来清除类的事例。当我们在C#中使用解构器是,我们必须记住以下几点:

     一个类只能有一个解构器。
     解构器不能被继承或重载。
     解构器不能被调用。他们是自动被(编译器)调用的。
     解构器不能带修饰或参数。


下面是类MyClass解构器的一个声明:

~ Class()   {      // Cleaning up code goes here  }

        程序员不能控制解构器何时将被执行因为这是由垃圾收集器决定的。垃圾收集器检查不在被应用程序使用的对象。它认为这些条件是符合清楚的并且收回它们的内存。解构器也在程序退出时被调用。当解构器执行时其背后所发生的那一幕是解构器隐式调用对象基类的Object.Finalize方法。因此上述解构器代码被隐含转化成:

protected override void Finalize()  {      try      {         // Cleaning up .      }      finally      {         base.Finalize();      }}

        现在,让我们看一个解构器怎样被调用的例子。我们有三个类A,B和C 。B派生自A,C派生自B。每个类有它们自己的构造器和解构。在类App的main函数中,我们创建C的对象。

using System;class A{    public A()    {        Console.WriteLine("Creating A");    }    ~A()    {        Console.WriteLine("Destroying A");    }}class B : A{    public B()    {        Console.WriteLine("Creating B");    }    ~B()    {        Console.WriteLine("Destroying B");    }}class C : B{    public C()    {        Console.WriteLine("Creating C");    }    ~C()    {        Console.WriteLine("Destroying C");    }}class App{    public static void Main()    {        C c = new C();        Console.WriteLine("Object Created ");        Console.WriteLine("Press enter to Destroy it");        Console.ReadLine();        c = null;        //GC.Collect();          Console.Read();    }}

正如我们预料的,基类的构造器将会被执行并且程序会等待用户按‘enter’。当这个发生,我们把类C的对象置为null.但解构器没有被执行..!!??正像我们所说的,程序员无法控制解构器何时被执行因为这是由垃圾搜集器决定的。但程序退出时解构器被调用了。你能通过重定向程序的o/p到文本文件来检查这个。我将它输出在这里。注意到基类的解构器被调用了,因为在背后base.Finalize()被调用了。

Creating ACreating BCreating CObject Created Press enter to Destroy itDestroying CDestroying BDestroying A

所以,如果一旦你使用完对象你就想调用解构器,你该怎么做?有两个方法:

调用垃圾搜集器来清理。

实现IDisposable的Dispose方法。

调用垃圾搜集器

     你能通过调用GC.Collect方法强制垃圾搜集器来清理内存,但在大多数情况下,这应该避免因为它会导致性能问题。在上面的程序中,在GC.Collect()处移除注释。编译并运行它。现在,你能看到解构器在控制台中被执行了。

    以过上面的分析,我们要明确,当CLR的垃圾收集线程要回收一个定义了析构函数的对象时,它会自动调用其Finalize方法。
析构函数存在的主要目的是释放非托管的资源。但是,对象的析构函数(即Finalize方法)被调用的时机是不可控的,因为它的调用由CLR的垃圾收集机制负责,出于性能考虑,CLR的垃圾收集线程只是在“它认为是合适的”时机才运行,这样一来就有可能出现对象所占有的非托管资源迟迟不能得到释放的情况。最好有一种方法能够让程序员主动地以完全可控的方式去释放这些非托管的资源,为此.NET提供了一个IDispose接口。

实现IDisposable接口

IDisposable 接口包括仅有的一个公共方法,其声明为void Dispose()。我们能实现这个方法来关闭或释放非托管资源如实现了这个接口的类事例所控制的文件,流,和句柄等。这个方法被用做所有任务联合对象的资源释放。当实现了这个方法,对象必须寻求确保所有拥有的资源被继承结构中关联的资源也释放(不能把握,翻不出来)。

class MyClass : IDisposable{    public void Dispose()    {        //implementation      }}

当我们实现了IDisposable接口时,我们需要规则来确保Dispose被适当地调用。
这里有一个问题,CLR不认为IDisposable接口与其他接口有什么不同,因CLR的垃圾收集线程不会主动地询问对象是否实现了IDisposable接口
并自动调用它的Dispose方法。另外,很多可能程序员在开发时会忘记调用对象的Dispose方法,为了避免资源泄露,我们让对象的析构函数也调用Dispose方法,

这就双保险了。如以下代码所示:

联合使用解构器和IDisposable接口

class MyClass : IDisposable{    private bool IsDisposed = false;    public void Dispose()    {        Dispose(true);        GC.SupressFinalize(this);    }    protected void Dispose(bool Diposing)    {        if (!IsDisposed)        {            if (Disposing)            {                //Clean Up managed resources              }            //Clean up unmanaged resources          }        IsDisposed = true;    }    ~MyClass()    {        Dispose(false);    }}

        这个代码框架看上去不太好理解,首先,给类添加一个Dispose(bool),此方法接收一个bool类型的参数,当参数值为true 时,可以编写一些必要的代码来清理托管的资源,比如调用本对象所引用的其他托管对象的Dispose方法。不管参数值如何,此Dispose(bool)方法都必须完成清理非托管资源的任务。

        当用户在应用 程序中显式调用IDisposable接口的Dispose方法时,此方法以true作为实参调用上面定义的虚方法Dispose(bool),并通知CLR的垃圾收集线程,不要再调用此对象的Finalize方法。

        如果用户没有显式调用IDisposable接口的Dispose方法,则由CLR的垃圾收集线程负责调用对象的Finalize方法完成资源清理工作,注意这时传入的参数是false,国为finalize方法只负责释放非托管的资源。

~MyClass(){    Dispose(false);}

        在这里重载了Dispose(bool)来做清理工作,并且所有的清理代码都仅写在这个方法中。这个方法被解构器和IDisposable.Dispose()两着调用。我们应该注意Dispose(bool)没有在任何地方被调用除了在IDisposable.Dispose()和解构器中。
        当一个客户调用IDisposable.Dispose()时,客户特意地想要清理托管的和非托管资源,并且因此完成清理工作。有一件你必须注意的事情是我们在清理资源之后立即调用了GC.SupressFinalize(this)。这个方法通知垃圾搜集器不需要调用解构器,因为我们已经做了清理。
        注意上面的例子,解构器使用参数false调用Dispose。这里,我们确信垃圾搜集器搜集了托管资源。我们仅仅做非托管资源的清理。

结论
尽管如此我们花费一些时间实现IDisposable接口,如果客户不能合适地调用它们会怎样?为此C#有一个酷的解决方案。“using”代码块。它看起来像这样:

using (MyClass objCls =new MyClass())  {     }

当控制从using块通过成功运行到结束或者抛出异常退出时,MyClass的IDispose.Dispose()将会被执行。记住你例示的对象必须实现System.IDisposable接口。using语句定义了哪个对象将被清除的一个范围。

注:
   构造函数与析构函数的区别:
   构造函数和析构函数是在类体中说明的两种特殊的成员函数。
   构造函数的功能是在创建对象时,使用给定的值来将对象初始化。
   析构函数的功能是用来释放一个对象的。在对象删除前,用它来做一些清理工作,它与构造函数的功能正好相反。

 

引读:

1.垃圾收集器(GC)控制着托管内存。但是我们需要对非托管代码进行处理(数据库连接,文件句柄,GDI+对象,COM对象等)

2.垃圾收集器是运行在一个单独的线程中来移除程序中不再使用的内存。并且还会压缩托管堆,使得空闲内存能集中在一个连续的区域。

3.对于非托管资源我们需要在类型中定义一个终结器,以此来确保释放这些系统资源。在对象的内存被回收之前,系统会调用终结器,这样我们可以在终结器中编写我们的代码来释放该对象占有的非托管资源。值得注意的是对象的终结器是在对象成为垃圾之后调用的,但是在内存归还之前,但是我们无法精确的控制“不再使用对象”和“执行终结器”这2个事件之间的关系。

4.当GC发现某个对象是垃圾但是需要终结器时,它还不能直接从内存上删除这个对象。首先,它要调用终结器,但终结器的调用不是在垃圾回收器的同一个线程上运行的。取而代之的是,GC不得不把对象放置到终结器队列中,让另一个线程让执行所有的终结器。GC继续它自己的工作,从内存上移除其它的垃圾。在下一个GC回收时,那些被终结器了的对象才会再从内存上移除。

5.Net回收器定义了一种称为“代龄”的机制来优化。代可以帮助GC来很快的 标识那些看上去看是垃圾的对象。所有从上一次GC回收后开始创建的对象称为第0代对象,所有那些经过一次GC回收后还存在的对象称为第1代对象。所有那些 经过2次或者2次以上GC回收后还存在的对象称为第2代对象。代龄的目的就是用来区分临时变量以及一些应用程序的全局变量。第0代对象绝大部分是临时的变 量。成员变量,以及一些全局变量很快会成为第1代对象,最终成为第2代对象。GC通过限制检测第1以及第2代对象的频度来优化它的工作。每一次GC循环都 检测第0代对象,大概10次GC周期才会检查第0和第一代对象,大概100次GC周期才检查一次第0,1,2代对象。让我们考虑一下:一个须要终结器的对 象可能要比一个不用终结器的对象在内存里多待上9个GC回收循环。如果它还没有终结器,它将会移到第2代对象。在第2代对象中,一个可以生存上100个 GC循环直到下一个第2代集合。

6.记得一个垃圾回收器负责内存管理的托管环境的最大好处:内存泄漏,其它指针的相关的问题将不再存在。非内存资源迫使你要使用终结器来确保清理非内存资源。终结器会对你的应用程序性能产生一些影响,但你必须使用它们来防止资源泄漏。通过实现IDisposable接口并且使用它可以避免终结器在垃圾回收器上造成的性能损失。