什么情况下需要实现IDisposeable接口?

1.前言

对.NET/C#稍有了解的同学,都应该知道IDispose模式的存在,但不知道有多少同学能彻彻底底地理解这种模式。楼主本人初识IDispose模式也有很长时间了,但对其设计原理和初衷也一直是云里雾里。直到这两天终于下定决心想彻底理解其工作模式,上网翻阅了不少资料,才算有所领悟,特别是StackOverFlow上的这篇文章:https://stackoverflow.com/questions/538060/proper-use-of-the-idisposable-interface/538238#538238,让我有茅舍顿开之感,极力推荐各位英文不错的同学参看此文。

2.你是否也有这些疑问:

  什么情况下需要实现IDisposeable接口?

  IDisposable模式与垃圾收集器(GC)之间到底有什么关联?

  isDisposing标志什么时候应该置为true,什么时候应该置为false,各自的意义是什么?

如果你也有如上疑问,希望看完本文,能给你一个满意的答复。

3. 托管资源和非托管资源

我们知道,在.NET平台下,代码中的资源类型包含两种:托管资源和非托管资源。你在.NET平台下创建的对象,以及你从FCL类库中调用的类型,一般都属于托管资源。只有windows窗体句柄、数据库连接以及网络连接(Socket)之类,以及你利用P/Invoke调用的Windows API属于非托管资源。

4. 非托管资源释放

如果你在一个(C#)类中创建了一个非托管资源,那么垃圾回收器将无法对其进行自动回收,这时候,程序员需要负责完成对该非托管资源的清理工作。作为类设计者,有多种方式可完成该项清理工作,比较常用的方法是,提供一个形如Clear()之类的清理接口函数,供类使用者调用,类设计者负责在该函数中完成清理工作,例如:


  
  
  1. namespace Sample
  2. {
  3. public class Sample
  4. {
  5. ....
  6. private intptr _handle;
  7. public void Clear()
  8. {
  9. CloseHandle(_handle);
  10. }
  11. }
  12. }

只要类使用者对该函数设计意图的理解没有偏差,在适当时机调用该函数以完成清理工作,那么这种方式就能实现非托管资源的释放。但是,.NET为我们设计了一种更好的模式 -- IDisposable接口:


  
  
  1. public interface IDisposable
  2. {
  3. void Dispose();
  4. }

有了IDisposable接口后,可按如下方式修改Sample类,使其更便于调用者使用(而不必去猜测Clear()函数的用意):


  
  
  1. namespace Sample
  2. {
  3. public class Sample: IDisposable
  4. {
  5. ....
  6. private intptr _handle;
  7. public void Dispose()
  8. {
  9. CloseHandle(_handle);
  10. }
  11. }
  12. }
这样,类使用者只要调用Dispose()函数,就可以完成类对象中非托管资源的清理。

5. Finalize()函数

我们通过约定(IDisposable接口)实现了非托管资源的清理工作。但这只是约定,其最终是否能达成,取决于类使用者。若使用者忘记了调用该接口,那么前面所做的工作都将成为徒劳,对象中的非托管资源仍将存在,并在该对象被销毁后处于无政府状态,这对于程序来说,是不可接受的。

因此,类设计者应采用某种机制,保证Dispose()函数一定被调用。这时,稍有经验的设计者,一般会想到C#类的Finalize()函数,该函数在对象被销毁时由垃圾收集器GC调用,用于清理对象中的资源,这种机制类似于C++的析构函数。在Finalize()函数中调用Dispose()函数,可保证非托管资源一定会在某个时机被清除。此时,样例类将变成如下样式:


  
  
  1. namespace Sample
  2. {
  3. public class Sample
  4. {
  5. ....
  6. private intptr _handle;
  7. public void Dispose()
  8. {
  9. CloseHandle(_handle);
  10. }
  11. ~Sample()
  12. {
  13. Dispose();
  14. }
  15. }
  16. }
注:C#中不可直接重写Finalize()函数,而应以类似于C++析构函数的方式实现,此处~Sample()等同于Finalize()函数

6. 清理托管资源

截止目前,我们基本实现(还有一点小问题,下文详述)了对象中非托管资源的清理。那么,对于托管资源我们是否应该一味得交由垃圾收集器来自动清理呢?假如,一个对象中托管资源占用了大量的内存资源,而垃圾收集器可能需要一段时间后才会光顾该对象,在这期间,我们就应该撒手不管,任由宝贵的内存资源被肆意侵占吗?我想这不是一个有良好职业操守的程序员愿意看到的。因此对于系统资源消耗较多或侵占关键资源的托管对象,我们也应该像处理非托管资源一样,对其进行手动清理,当然这只是建议,并非必须如此。

最直接清理托管资源的方法,就是在Dispose()函数中,加入对其进行的清理操作,如:


  
  
  1. namespace Sample
  2. {
  3. public class Sample
  4. {
  5. .....
  6. private intptr _handle;
  7. private object _managedObj;
  8. public void Dispose()
  9. {
  10. CloseHandle(_handle);
  11. if(_managedObj != null)
  12. _managedObj = nu;//之后GC将会自动将该对象销毁
  13. }
  14. }
  15. }
7. isDisposing标志

上述对托管资源的清理操作,可能引发问题。如果Dispose()方法是由垃圾收集器(GC)在通过Finalize()函数调用,那么,这时候GC已经销毁了该对象中的托管资源,此时再调用_managedObj.Dispose()可能引发异常(虽然之前已经做了null判断,但仍有此可能),即使不引发任何异常,这种重复操作也是多此一举。

因此,我们需要一个标志位来判别当前是谁在调用Dispose()函数,这个标志位就是isDisposing, 当其为True时,说明是调用者通过IDisposable接口在调用该函数;当前为false时(相当于isFinalizing),说明当前是由Finalize()函数在调用该函数,也即是由GC在调用。

加入isDisposing标志后,Dispose函数应形如:void Dispose(bool isDisposing);但此签名与IDisposable接口定义相违,所以我们定义一个第三方函数,供IDisposable接口和Finalize()函数调用。约定俗成,这个第三方函数仍旧命名为Dispose(),只是其访问属性和签名有所区别,加入该函数后,IDisposable模式将形如:


  
  

   
   
  1. namespace Sample
  2. {
  3. public class Sample
  4. {
  5. ....
  6. private intptr _handle;
  7. private object _managedObj;
  8. public void Dispose()
  9. {
  10. Dispose( true); //调用第三方函数
  11. }
  12. protect virtual void Dispose(bool isDisposing)
  13. {
  14. CloseHandle(_handle);
  15. if(isDisposing)
  16. {
  17. if(_managedObj != null)
  18. _managedObj = null;
  19. }
  20. }
  21. ~Sample()
  22. {
  23. Dispose( false);
  24. }
  25. }
  26. }

 
 

8. GC.SuppressFinalize()

经过一系列优化后,上述样例代码已趋于完善,但仍有一个比较大的问题:若类使用者在某个时机调用Dispose()函数,销毁了对象中的非托管资源,而当该对象不再被引用后,垃圾收集器GC将在某个时机调用Finalize()函数,即再次调用了Dispose()函数,但此时非托管资源已不存在,再次对其进行清理将引发bug。

对于以上问题,.NET的垃圾收集机制提供了SuppressFinalize()函数,调用该函数相当于通知垃圾收集器不要再调用该对象的Finalize函数。因此,我们应在对外接口函数Dispose()中,加入GC.SuppressFinalize(),加入后形如:


  
  
  1. namespace Sample
  2. {
  3. public class Sample
  4. {
  5. ....
  6. private intptr _handle;
  7. private object _managedObj;
  8. public void Dispose()
  9. {
  10. Dispose(True);
  11. GC.SuppressFinalize();
  12. }
  13. protect virtual void Dispose(bool isDisposing)
  14. {
  15. CloseHandle(_handle);
  16. if(isDisposing)
  17. {
  18. if(_managedObj != null)
  19. _managedObj = null;
  20. }
  21. }
  22. ~Sample()
  23. {
  24. Dispose( false);
  25. }
  26. }
  27. }

9. isDisposed标志

最后,一般为了防止重复清理,会在IDisposable模式加入isDisposed标志。处理方式如下:


  
  
  1. namespace Sample
  2. {
  3. public class Sample
  4. {
  5. ....
  6. private intptr _handle;
  7. private object _managedObj;
  8. private bool _isDisposed;
  9. public void Dispose()
  10. {
  11. Dispose( true);
  12. GC.SuppressFinalize();
  13. }
  14. protect virtual void Dispose(bool isDisposing)
  15. {
  16. if(!_isDisposed)
  17. {
  18. CloseHandle(_handle);
  19. if(isDisposing)
  20. {
  21. if(_managedObj != null)
  22. _managedObj = null;
  23. }
  24. _isDisposed = true;
  25. }
  26. ~Sample
  27. {
  28. Dispose( false);
  29. }
  30. }
  31. }
  32. }

10.总结

最后,对一开始提出的问题进行一下回答:

什么情况下需要实现IDisposable接口:类中存在非托管资源时,就应该实现该接口;类中存在消耗系统资源较多的托管对象时,建议实现该接口。

IDisposable模式与GC之间有什么联系:二者并无实际联系,但在Dispose接口函数中应调用GC.SuppressFinalize()函数通知GC不要再次清理

isDisposing标志什么时候应该置为true,什么时候应该置为false:通过Dispose接口函数调用时,应为True,否则应为false。


发布了65 篇原创文章 · 获赞 34 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/My_ben/article/details/103234566