为什么要实现 IDisposable 接口?
一、背景
最近在精读 《clr via c#》和 《effective c#》 的时候,发现的一个问题点。一般来说,我们实现 idisposable
接口,是为了释放托管资源和非托管资源。不过在 c# 类型定义里面有一个功能类似的东西,那就是 终结器。
最开始我是学 c++ 的,之后学 c# 的时候发现这玩意儿不论是写法和作用,都跟 c++ 里面的 析构函数 一样。在 c++ 里面的析构函数是在对象释放的时候会被调用,之后这个观点一直被我带到 c#,认为资源释放的动作放在终结器不就行了么。为什么还要我实现 idisposable
接口,然后让使用者手动释放呢?
c++ 版本的析构函数:
class line { public: line(); ~line(); private: double length; };
c# 版本的终结器:
public class line { private double _length; public line() { } ~line() { } }
二、原因
说起这个原因,首先得从 c# 终结器的 调用时机 说起。终结器的调用是 clr 在进行 gc 时,如果某个对象写有终结器,即便它应该被释放,也不会马上回收该对象。而 c++ 的析构函数是确定性析构,取决于你调用 delete 的时机。
gc 会将其添加到一个队列当中,单独使用了一个 高优先级 线程去调用对象的终结器。因为要保证线程能够访问到终结器对象,所以本该释放的对象,以及对象相关的资源就 会被提升 1 代 ,会 增加内存占用。
一旦终结器方法带有死循环,那么 gc 将永远无法释放该资源,造成 内存泄漏。
除开内存占用增大的原因,如果你在终结器方法内部引用了其他带终结器对象,gc 无法保证终结器调用顺序,所以你可能访问到的对象是已经终结了的。
还有一种情况会导致尴尬的内存泄漏,本来对象 a 应该被释放了,结果你在终结器内部又让其他的根保持对象的引用,又会让这个对象复活。因为 gc 只会执行一次带终结器对象的终结器。执行一次过后,就再也不会执行对象的终结器了。
public class badclass { private static readonly list<badclass> _list = new list<badclass>(); private string _msg; public badclass(string msg) { _msg = (string)msg.clone(); } ~badclass() { // 造成 _msg 的内存不会被释放。 _list.add(this); } }
三、最佳实践
针对 effective c# 所提出的最佳实践,你应该为对象实现 idisposable
接口,以释放托管资源。如果你对象确实使用了非托管资源,那么你也应该为其编写终结器。因为非托管资源的,你不能保证调用者能够显示调用 dispose()
方法,所以你得通过终结器来处理。
一个典型的 dispose()
方法应该将托管资源、非托管资源全部进行释放,设置对应的标识表明对象已经被释放了,阻止垃圾回收器重复清理该对象、保证方法的 幂等性。
public class fatherclass : idisposable { private bool isdisposed = false; public void dispose() { dispose(true); // 通知 gc,这个对象已经完全被清理。 gc.suppressfinalize(this); } ~fatherclass() { dispose(false); } protected virtual dispose(bool isdisposing) { if(isdisposed) return; if(isdisposing) { // 释放托管资源。 } // 释放非托管资源。 isdisposed = true; } public void testmethod() { if(isdisposed) { throw new objectdisposedexception("对象已经被释放。"); } } } public class childclass : fatherclass { private bool isdisposed = false; protected override void dispose(bool isdisposing) { if(isdisposed) return; if(isdisposing) { // 释放托管资源。 } base.dispose(isdisposing); isdisposed = true; } }
在上面的实践中,我们提炼出了一个 void dispose(bool)
方法,并将其设置为虚函数。这样做的好处有两点,第一点是方便子类重写释放逻辑,第二点是可以将终结器和 dispose()
方法内部重复的代码提炼出来。
上一篇: 被俄国占领的那块土地为何一直没有收复?还能收回吗?
下一篇: 肉蟹怎么杀?简单几步就能将肉蟹处理好
推荐阅读
-
为什么要实现 IDisposable 接口?
-
Mybaits 源码解析 (五)----- 面试源码系列:Mapper接口底层原理(为什么Mapper不用写实现类就能访问到数据库?)
-
Mybaits 源码解析 (三)----- Mapper接口底层原理(为什么Mapper不用写实现类就能访问到数据库?)
-
Java中为什么实体类需要实现Serializable序列化接口
-
java持久化类为什么要实现序列化
-
java持久化类为什么要实现序列化
-
Mybatis DAO接口为什么不需要实现类
-
Entity实体类为什么要实现Serializable接口才能被序列化
-
为什么要实现 IDisposable 接口?
-
Mybaits 源码解析 (五)----- 面试源码系列:Mapper接口底层原理(为什么Mapper不用写实现类就能访问到数据库?)