一、背景

最近在精读 《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() 方法内部重复的代码提炼出来。

最新文章

  1. Qt: 时钟Demo
  2. Linux环境安装Eclipse及配置hadoop插件
  3. Git 基础
  4. Tornado实战项目(伪JD商城)
  5. Python 字符串操作
  6. JQuery源码解析(十一)
  7. 第三天:DOM EventListener 句柄的添加和移除
  8. VS2015环境下Crystal Reports(水晶报表)的安装使用
  9. python3.0与2.x之间的区别
  10. Intellij Idea 创建Web项目入门(一)
  11. 如何创建phpinfo查看php信息?
  12. Java虚拟机--虚拟机编译器
  13. Oracle官方版Entity Framework
  14. 数据字典的QUAN DEC类型与ABAP P型转换
  15. IOS开发根据字体大小等获取文字所占的高度
  16. canvas与svg区别
  17. ArcSDE数据库连接(直连、服务连)与GT_Geometry存储配置图解
  18. Winform文件上传
  19. 【Android】1.2 创建Android模拟器
  20. MyBatis整合Spring MVC

热门文章

  1. java.util.Timer简介
  2. 数据可视化之3D中国
  3. JAVA运行内部类的main方法
  4. TestNG(十一) 超时测试
  5. TestNG(五) 5-7 套件测试
  6. activity的隐式和显式启动
  7. SpringBoot整合Nacos注册中心
  8. LoadRunner11.安装破解
  9. 第十一周java课堂测试
  10. maven 3.6的安装