深刻理解C#中的IDisposable接口

写在前面

在开始以前,咱们须要明确什么是C#(或者说.NET)中的资源,打码的时候咱们常常说释放资源,那么到底什么是资源,简单来说,C#中的每一种类型都是一种资源,而资源又分为托管资源和非托管资源,那这又是什么?!html

托管资源:由CLR管理分配和释放的资源,也就是咱们直接new出来的对象;java

非托管资源:不受CLR控制的资源,也就是不属于.NET自己的功能,每每是经过调用跨平台程序集(如C++)或者操做系统提供的一些接口,好比Windows内核对象、文件操做、数据库链接、socket、Win32API、网络等。git

咱们下文讨论的,主要也就是非托管资源的释放,而托管资源.NET的垃圾回收已经帮咱们完成了。其实非托管资源有部分.NET的垃圾回收也帮咱们实现了,那么若是要让.NET垃圾回收帮咱们释放非托管资源,该如何去实现。web

 

如何正确的显式释放资源

假设咱们要使用FileStream,咱们一般的作法是将其using起来,或者是更老式的try…catch…finally…这种作法,由于它的实现调用了非托管资源,因此咱们必须用完以后要去显式释放它,若是不去释放它,那么可能就会形成内存泄漏。数据库

这听上去貌似很简单,但咱们编码的时候可能不少时候会忽略掉释放资源这个问题,.NET的垃圾回收又如何帮咱们释放非托管资源,接下来咱们一探究竟吧,一个标准的释放非托管资源的类应该去实现IDisposable接口:c#

1
2
3
4
5
6
7
public  class  MyClass:IDisposable
{
     /// <summary>执行与释放或重置非托管资源关联的应用程序定义的任务。</summary>
     public  void  Dispose()
     {
     }
}

咱们实例化的时候就能够将这个类using起来:缓存

1
2
3
using (var mc =  new  MyClass())
{
}

看上去很简单嘛,可是,要是就这么简单的话,也没有这篇文章的必要了。若是要实现IDisposable接口,咱们其实应该这样作:网络

  1. 实现Dispose方法;dom

  2. 提取一个受保护的Dispose虚方法,在该方法中实现具体的释放资源的逻辑;socket

  3. 添加析构函数;

  4. 添加一个私有的bool类型的字段,做为释放资源的标记

接下来,咱们来实现这样的一个Dispose模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public  class  MyClass : IDisposable
{
     /// <summary>
     /// 模拟一个非托管资源
     /// </summary>
     private  IntPtr NativeResource {  get set ; } = Marshal.AllocHGlobal(100);
     /// <summary>
     /// 模拟一个托管资源
     /// </summary>
     public  Random ManagedResource {  get set ; } =  new  Random();
     /// <summary>
     /// 释放标记
     /// </summary>
     private  bool  disposed;
     /// <summary>
     /// 为了防止忘记显式的调用Dispose方法
     /// </summary>
     ~MyClass()
     {
         //必须为false
         Dispose( false );
     }
     /// <summary>执行与释放或重置非托管资源关联的应用程序定义的任务。</summary>
     public  void  Dispose()
     {
         //必须为true
         Dispose( true );
         //通知垃圾回收器再也不调用终结器
         GC.SuppressFinalize( this );
     }
     /// <summary>
     /// 非必需的,只是为了更符合其余语言的规范,如C++、java
     /// </summary>
     public  void  Close()
     {
         Dispose();
     }
     /// <summary>
     /// 非密封类可重写的Dispose方法,方便子类继承时可重写
     /// </summary>
     /// <param name="disposing"></param>
     protected  virtual  void  Dispose( bool  disposing)
     {
         if  (disposed)
         {
             return ;
         }
         //清理托管资源
         if  (disposing)
         {
             if  (ManagedResource !=  null )
             {
                 ManagedResource =  null ;
             }
         }
         //清理非托管资源
         if  (NativeResource != IntPtr.Zero)
         {
             Marshal.FreeHGlobal(NativeResource);
             NativeResource = IntPtr.Zero;
         }
         //告诉本身已经被释放
         disposed =  true ;
     }
}

这里面每行代码都有它独自的含义,文章里不可能每句话都讲解透彻,为了突出重点,因此接下来就挑出几个重要的地方逐一解释咯,固然截止如今,咱们只须要记住:

若是类型须要显式的释放资源,那么就必定要实现IDisposable接口。

实现IDisposable接口其实也是为了方便使用using这个语法糖,以方便编译器帮咱们自动生成Dispose的IL代码:

1
2
3
using (var mc =  new  MyClass())
{
}

就至关于:

1
2
3
4
5
6
7
8
9
10
11
12
MyClass mc =  null ;
try
{
     mc =  new  MyClass();
}
finally
{
     if  (mc !=  null )
     {
         mc.Dispose();
     }
}

若是要同时管理多个相同类型的对象:

1
2
3
using (MyClass mc1= new  MyClass(),mc2= new  MyClass())
{
}

若是类型不一致:

1
2
3
4
5
6
using (var client =  new  HttpClient())
{
     using (var stream = File.Create( "" ))
     {
     }
}
 

为何须要析构方法?

在以前咱们实现的更标准的Dispose模式中,咱们注意到了,类里面包含了一个~开头的析构方法:

1
2
3
4
5
~MyClass()
{
     //必须为false
     Dispose( false );
}

这个析构方法更规范的说法叫作终结器,它的意义在于,若是咱们忘记了显式调用Dispose方法,垃圾回收器在扫描内存的时候,会做为释放资源的一种补救措施。

为何加了析构方法就会有这种效果,咱们知道在new对象的时候,CLR会为对象建立一块内存空间,一旦对象再也不被引用,就会被垃圾回收器回收掉,对于没有实现IDisposable接口的类来讲,垃圾回收时将直接回收掉这片内存空间,而对于实现了IDisposable接口的类来讲,因为析构方法的存在,在建立对象之初,CLR会将该对象的一个指针放到终结器列表中,在GC回收内存以前,会首先将终结器列表中的指针放到一个freachable队列中,同时,CLR还会分配专门的内存空间来读取freachable队列,并调用对象的终结器,只有在这个时候,对象才被真正的被标识为垃圾,在下一次垃圾回收的时候才回收这个对象所占用的内存空间。

那么,实现了IDisposable接口的对象在回收时要通过两次GC才能被真正的释放掉,由于GC要先安排CLR调用终结器,基于这个特色,若是咱们显式调用了Dispose方法,那么GC就不会再进行第二次垃圾回收了,固然,若是忘记了Dispose,也避免了忘记调用Dispose方法形成的内存泄漏。

提示:析构方法是在C++中的一种说法,由于终结器和析构方法二者特色很像,为了沿袭C++的叫法,称之为析构方法也没有什么不妥,但它们又不彻底一致,因此微软后来又肯定它叫终结器。

还有一点咱们也注意到了,若是已经显式的调用了Dispose方法,那么隐式释放资源就再不必运行了,GC的SuppressFinalize方法就是通知GC的这一点:

1
2
3
4
5
6
7
public  void  Dispose()
{
     //必须为true
     Dispose( true );
     //通知垃圾回收器再也不调用终结器
     GC.SuppressFinalize( this );
}

因此在实现的Dispose方法中先调用咱们正常的资源释放代码,再通知GC不要调用终结器了。

 

为何须要提供一个Dispose虚方法?

咱们注意到了,实现自Idisposable接口的Dispose方法并无作实际的清理工做,而是调用了咱们这个受保护的Dispose虚方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
protected  virtual  void  Dispose( bool  disposing)
{
     if  (disposed)
     {
         return ;
     }
     //清理托管资源
     if  (disposing)
     {
         if  (ManagedResource !=  null )
         {
             ManagedResource =  null ;
         }
     }
     //清理非托管资源
     if  (NativeResource != IntPtr.Zero)
     {
         Marshal.FreeHGlobal(NativeResource);
         NativeResource = IntPtr.Zero;
     }
     //告诉本身已经被释放
     disposed =  true ;
}

之因此是虚方法,就是考虑到它若是被其余类继承时,子类也实现了Dispose模式,这个虚方法能够提醒子类,清理的时候要注意到父类的清理工做,即若是子类从新该方法,必须调用base.Dispose方法,假设如今咱们有个子类,继承自MyClass:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public  class  MyClassChild : MyClass
{
     /// <summary>
     /// 模拟一个非托管资源
     /// </summary>
     private  IntPtr NativeResource {  get set ; } = Marshal.AllocHGlobal(100);
     /// <summary>
     /// 模拟一个托管资源
     /// </summary>
     public  Random ManagedResource {  get set ; } =  new  Random();
     /// <summary>
     /// 释放标记
     /// </summary>
     private  bool  disposed;
     /// <summary>
     /// 非密封类可重写的Dispose方法,方便子类继承时可重写
     /// </summary>
     /// <param name="disposing"></param>
     protected  override  void  Dispose( bool  disposing)
     {
         if  (disposed)
         {
             return ;
         }
         //清理托管资源
         if  (disposing)
         {
             if  (ManagedResource !=  null )
             {
                 ManagedResource =  null ;
             }
         }
         //清理非托管资源
         if  (NativeResource != IntPtr.Zero)
         {
             Marshal.FreeHGlobal(NativeResource);
             NativeResource = IntPtr.Zero;
         }
         base .Dispose(disposing);
     }
}

若是不是虚方法,那么就颇有可能让开发者在子类继承的时候忽略掉父类的清理工做,因此,基于继承体系的缘由,咱们要提供这样的一个虚方法。

其次,提供的这个虚方法是一个带bool参数的,带这个参数的目的,是为了释放资源时区分对待托管资源和非托管资源,而实现自IDisposable的Dispose方法调用时,传入的是true,而终结器调用的时候,传入的是false,当传入true时表明要同时处理托管资源和非托管资源;而传入false则只须要处理非托管资源便可。

那为何要区别对待托管资源和非托管资源?在这个问题以前,其实咱们应该先弄明白:托管资源须要手动清理吗?不妨将C#的类型分为两类:一类实现了IDisposable,另外一类则没有。前者咱们定义为非普通类型,后者为普通类型。非普通类型包含了非托管资源,实现了IDisposable,但又包含有自身是托管资源,因此不普通,对于咱们刚才的问题,答案就是:普通类型不须要手动清理,而非普通类型须要手动清理。

而咱们的Dispose模式设计思路在于:若是显式调用Dispose,那么类型就该循序渐进的将本身的资源所有释放,若是忘记了调用Dispose,那就假定本身的全部资源(哪怕是非普通类型)都交给GC了,因此不须要手动清理,因此这就理解为何实现自IDisposable的Dispose中调用虚方法是传true,终结器中传false了。

同时咱们还注意到了,虚方法首先判断了disposed字段,这个字段用于判断对象的释放状态,这意味着屡次调用Dispose时,若是对象已经被清理过了,那么清理工做就不用再继续。

但Dispose并不表明把对象置为了null,且已经被回收完全不存在了。但事实上,对象的引用还可能存在的,只是再也不是正常的状态了,因此咱们明白有时候咱们调用数据库上下文有时候为何会报“数据库链接已被释放”之类的异常了。

因此,disposed字段的存在,用来表示对象是否被释放过。

 

若是对象包含非托管类型的字段或属性的类型应该是可释放的

这句话读起来可能有点绕啊,也就是说,若是对象的某些字段或属性是IDisposable的子类,好比FileStream,那么这个类也应该实现IDisposable。

以前咱们说过C#的类型分为普通类型和非普通类型,非普通类型包含普通的自身和非托管资源。那么,若是类的某个字段或属性的类型是非普通类型,那么这个类型也应该是非普通类型,应该也要实现IDisposable接口。

举个栗子,若是一个类型,组合了FileStream,那么它应该实现IDisposable接口,代码以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public  class  MyClass2 : IDisposable
{
     ~MyClass2()
     {
         Dispose( false );
     }
     public  FileStream FileStream {  get set ; }
     /// <summary>
     /// 释放标记
     /// </summary>
     private  bool  disposed;
     /// <summary>执行与释放或重置非托管资源关联的应用程序定义的任务。</summary>
     public  void  Dispose()
     {
         Dispose( true );
         GC.SuppressFinalize( this );
     }
     /// <summary>
     /// 非密封类可重写的Dispose方法,方便子类继承时可重写
     /// </summary>
     /// <param name="disposing"></param>
     protected  virtual  void  Dispose( bool  disposing)
     {
         if  (disposed)
         {
             return ;
         }
         //清理托管资源
         if  (disposing)
         {
             //todo
         }
         //清理非托管资源
         if  (FileStream !=  null )
         {
             FileStream.Dispose();
             FileStream =  null ;
         }
         //告诉本身已经被释放
         disposed =  true ;
     }
}

由于类型包含了FileStream类型的字段,因此它包含了非普通类型,咱们仍旧须要为这个类型实现IDisposable接口。

 

及时释放资源

可能不少人会问啊,GC已经帮咱们隐式的释放了资源,为何还要主动地释放资源,咱们先来看一个例子:

1
2
3
4
5
6
7
8
private  void  button6_Click( object  sender, EventArgs e)
{
     var fs =  new  FileStream( @"C:\1.txt" ,FileMode.OpenOrCreate,FileAccess.ReadWrite);
}
private  void  button7_Click( object  sender, EventArgs e)
{
     GC.Collect();
}

上面的代码在WinForm程序中,单击按钮6,打开一个文件流,单击按钮7执行GC回收全部“代”(下文将指出代的概念)的垃圾,若是连续单击两次按钮6,将会抛异常:

若是单击按钮6再单击按钮7,而后再单击按钮6则不会出现这个问题。

咱们来分析一下:在单击按钮6的时候打开一个文件,方法已经执行完毕,fs已经没有被任何地方引用了,因此被标记为了垃圾,那么何时被回收呢,或者GC何时开始工做?微软官方的解释是,当知足如下条件之一时,GC才会工做:

  1. 系统具备较低的物理内存;

  2. 由托管堆上已分配的对象使用的内存超出了可接受的范围;

  3. 手动调用GC.Collect方法,但几乎全部的状况下,咱们都没必要调用,由于垃圾回收器会自动调用它,但在上面的例子中,为了体验一下不及时回收垃圾带来的危害,因此手动调用了GC.Collect,你们也能够仔细体会一下运行这个方法带来的不一样。

GC还有个“代”的概念,一共分3代:0代、1代、2代。而这三代,至关因而三个队列容器,第0代包含的是一些短时间生存的对象,上面的例子fs就是个短时间对象,当方法执行完后,fs就被丢到了GC的第0代,但不进行垃圾回收,只有当第0代满了的时候,系统认为此时知足了低内存的条件,才会触发垃圾回收事件。因此咱们永远不知道fs何时被回收掉,在回收以前,它实际上已经没有用处了,但始终占着系统资源不放(占着茅坑不拉屎),这对系统来讲是种极大的浪费,并且这种浪费还会干扰整个系统的运行,好比咱们的例子,因为它始终占着资源,就致使了咱们不能再对文件进行访问了。

不及时释放资源还会带来另外的一个问题,虽然以前咱们说实现IDisposable接口的类,GC能够自动帮咱们释放,但这个过程被延长了,由于它不是在一次回收中完成全部的清理工做,即便GC自动帮咱们释放了,那也是先调用FileStream的终结器,在下一次的垃圾回收时才会真正的被释放。

了解到危害后,咱们在打码过程当中,若是咱们明知道它应该被using起来时,必定要using起来:

1
2
3
using  (var fs =  new  FileStream( @"C:\1.txt" , FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
}
 

需不须要将再也不使用的对象置为null

在上文的内容中,咱们都提到要释放资源,但并无说明需不须要将再也不使用的对象置为null,而这个问题也是一直以来争议很大的问题,有人认为将对象置为null能让GC更早地发现垃圾,也有人认为这并无什么卵用。其实这个问题首先是从方法的内部被提起的,为了更好的说明这个问题,咱们先来段代码来检验一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private  void  button6_Click( object  sender, EventArgs e)
{
     var mc1 =  new  MyClass() { Name =  "mc1"  };
     var mc2 =  new  MyClass() { Name =  "mc2"  };
     mc1 =  null ;
}
private  void  button7_Click( object  sender, EventArgs e)
{
     GC.Collect();
}
public  class  MyClass
{
     public  string  Name {  get set ; }
     ~MyClass()
     {
         MessageBox.Show(Name +  "被销毁了" );
     }
}

单击按钮6,再单击按钮7,咱们发现:

没有置为null的mc2会先被释放,虽然它在mc1被置为null以后;

在CLR托管的应用程序中,有一个“根”的概念,类型的静态字段、方法参数以及局部变量均可以被做为“根”存在(值类型不能做为“根”,只有引用类型才能做为“根”)。

上面的代码中,mc1和mc2在代码运行过程当中分别会在内存中建立一个“根”。在垃圾回收的过程当中,GC会沿着线程栈扫描“根”(栈的特色先进后出,也就是mc2在mc1以后进栈,但mc2比mc1先出栈),检查完毕后还会检查全部引用类型的静态字段的集合,当检查到方法内存在“根”时,若是发现没有任何一个地方引用这个局部变量的时候,无论你是否已经显式的置为null这都意味着“根”已经被中止,而后GC就会发现该根的引用为空,就会被标记为可被释放,这也表明着mc1和mc2的内存空间能够被释放,因此上面的代码mc1=null没有任何意义(方法的参数变量也是如此)。

其实.NET的JIT编译器是一个优化过的编译器,因此若是咱们代码里面将局部变量置为null,这样的语句会被忽略掉:

s=null;

若是咱们的项目是在Release配置下的,上面的代码压根就不会被编译到dll,正是因为咱们上面的分析,因此不少人都会认为将对象赋值为null彻底没有必要,可是,在另外一种状况下,就彻底有必要将对象赋值为null,那就是静态字段或属性,但这斌不意味着将对象赋值为null就是将它的静态字段赋值为null:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private  void  button6_Click( object  sender, EventArgs e)
{
     var mc =  new  MyClass() { Name =  "mc"  };
}
private  void  button7_Click( object  sender, EventArgs e)
{
     GC.Collect();
}
public  class  MyClass
{
     public  string  Name {  get set ; }
     public  static  MyClass2 MyClass2 {  get set ; } =  new  MyClass2();
     ~MyClass()
     {
         //MyClass2 = null;
         MessageBox.Show(Name +  "被销毁了" );
     }
}
public  class  MyClass2
{
     ~MyClass2()
     {
         MessageBox.Show( "MyClass2被释放" );
     }
}

上面的代码运行咱们会发现,当mc被回收时,它的静态属性并无被GC回收,而咱们将MyClass终结器中的MyClass2=null的注释取消,再运行,当咱们两次点击按钮7的时候,属性MyClass2才被真正的释放,由于第一次GC的时候只是在终结器里面将MyClass属性置为null,在第二次GC的时候才看成垃圾回收了,之因此静态变量不被释放(即便赋值为null也不会被编译器优化),是由于类型的静态字段一旦被建立,就被做为“根”存在,基本上不参与GC,因此GC始终不会认为它是个垃圾,而非静态字段则不会有这样的问题。

因此在实际工做当中,一旦咱们感受静态变量所占用的内存空间较大的时候,而且不会再使用,即可以将其置为null,最典型的案例就是缓存的过时策略的实现了,将静态变量置为null这或许不是颇有必要,但这绝对是一个好的习惯,试想一个项目中,若是将某个静态变量做为全局的缓存,若是没有作过时策略,一旦项目运行,那么它所占的内存空间只增不减,最终顶爆机器内存,因此,有个建议就是:尽可能地少用静态变量。

 

出处:http://www.javashuo.com/article/p-vumndyjl-bb.html

相关文章
相关标签/搜索