最近在精读 《CLR Via C#》和 《Effective C#》 的时候,发现的一个问题点。通常来讲,咱们实现 IDisposable
接口,是为了释放托管资源和非托管资源。不过在 C# 类型定义里面有一个功能相似的东西,那就是 终结器。ide
最开始我是学 C++ 的,以后学 C# 的时候发现这玩意儿不管是写法和做用,都跟 C++ 里面的 析构函数 同样。在 C++ 里面的析构函数是在对象释放的时候会被调用,以后这个观点一直被我带到 C#,认为资源释放的动做放在终结器不就好了么。为何还要我实现 IDisposable
接口,而后让使用者手动释放呢?函数
C++ 版本的析构函数:this
class Line { public: Line(); ~Line(); private: double length; };
C# 版本的终结器:线程
public class Line { private double _length; public Line() { } ~Line() { } }
提及这个缘由,首先得从 C# 终结器的 调用时机 提及。终结器的调用是 CLR 在进行 GC 时,若是某个对象写有终结器,即使它应该被释放,也不会立刻回收该对象。而 C++ 的析构函数是肯定性析构,取决于你调用 delete 的时机。code
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()
方法内部重复的代码提炼出来。