C#中IDispose接口的实现及为何这么实现详解

前言

我原本认为对于IDispose的实现方法,只要在里面释放非托管资源就行了,但是通过网上资料,看到很多实现方法并不是仅仅做释放非托管资源,非常迷惑,关键是这些资料也没详细的告诉你为什么这么做?之后通过StackOverflow了解到这一步一步的原因,说的十分详细,结合自己的认识,翻译后分享给大家:

一、IDispose的实现方法

具体的实现方法,你可以直接查看这个我们网站的教程:

//www.jb51.net/article/54899.htm

如果你能看懂,并且很清楚为什么那么做。那么以下的文章你就可以略去不看。如果不清楚为什么那么做,请带着你的迷惑往下看:

二、为什么那样实现

英文好的可以直接去StackOverflow原文地址:

https://stackoverflow.com/questions/538060/proper-use-of-the-idisposable-interface/538238#538238

2.1、进行之前

在C++中,所有你在堆上申请的内存空间,必须手动释放掉,否则就会造成内存的泄露。这可能会让你在写程序的时候要花点心思在内存的管理上而不是专注于解决你编程的目的—解决问题。所以作为C++的进化版C#使用了GC(Garbage Collector)来进行内存的管理以达到自动释放不需要的内存的目的,但是GC并不能做的十分完美,对于一些非托管资源,GC无能为力,这就要求我们必须手动的释放那么非托管资源,为了更好的去做到这一点,我们就要编写一种方法,通过手动调用这个方法,我们就能够释放掉非托管资源。

注:

什么是托管资源和非托管资源?

托管资源就是托管给CLR的资源,CLR能对这些资源进行管理。而非托管资源则是CLR无法对这些资源管理,这些资源的申请、释放必须由使用者自行管理。

例如,像Win32编程中的文件句柄,上下文句柄、窗口或网络连接等资源都属于非托管资源。但是如果这些非托管资源在.Net中进行了封装,成为了.Net类库中的一部分,它就不属于非托管资源了,因为在对它们封装的过程中,就实现了它们的自动管理功能。

也就是说,你能在.Net中找到的类产生的对象,都是托管资源。

(理解这点很重要,这可能是你看不懂上面实现教程的重要一个原因!)

注:

GC进行垃圾回收的时间和顺序?

GC进行垃圾回收的时间我们根本无法确定(当然你手动调用GC的垃圾回收方法除外),并且顺序也不能确定!也就是说,你先申请的空间有可能在你后申请的空间释放之后释放。

GC对于实现析构函数和没实现析构函数的类处理方法不一样,简单些说GC对于实现了析构函数的类一定会调用他们的析构函数。

关于.Net的垃圾回收机制,你可以暂时先知道这么多,待看完了这篇文章再去深入了解。

2.2、我们需要编写一种方法去释放!

为了去清除一些非托管资源,你创建的类需要有一个public方法,方法的名字可以随意命名

例如:

public void Cleanup()
public void Shutdown()
…… 

你可以这么做,但是有一个标准的名字

public void Dispose() 

甚至有一个接口IDisposeable,里面包含的就是刚才那个方法

public interface IDisposable
{
 void Dispose()
} 

因此最好的办法是让你的类去实现IDisposable接口,在接口内的Dispose方法内提供一段清除非托管资源的代码。

public void Dispose()
{
 //这里释放一个句柄(句柄是一个非托管资源,属于Win32编程的概念)
 Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);
} 

OK。这就完成了,除非你想做的更好!

2.3、别忘了类中的托管资源还占着空间!

托管资源占着空间?你首先想到的可能是那些int,string等等这些托管资源,它们能占用几个空间,他们占着就占着呗!

但是托管资源可不仅仅是那些资源,要是你的对象使用了250MB的System.Drawing,Bitmap(这是在.Net Frame中的,属于托管资源)作为一些缓冲怎么办?当然,你知道这是一个.Net的托管资源,所以GC理所应当的将会释放它。但是你真的想留着250MB的内存空间就那么被占用着?然后等待着GC最终释放它?更或者要是有一个更大数据库连接呢?我们当然不想让那连接白白占用来等待GC的终结!

如果用户调用了Dispose方法(意味着他们不再想使用这个对象里的一切)为什么不去扔掉那些浪费空间的位图资源和数据库连接呢?

那么,我们就应该这么做:

  • 释放非托管资源(因为我们必须这么做)
  • 释放托管资源(让你的Dispose更完美)

所以,让我们更新我们的Dispose方法来释放那些托管资源

public void Dispose()
{
 //Free unmanaged resources
 Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle); 

 //Free managed resources too
 if (this.databaseConnection !=null)
 {
 this.databaseConnection.Dispose();
 this.databaseConnection =null;
 }
 if (this.frameBufferImage !=null)
 {
 this.frameBufferImage.Dispose();
 this.frameBufferImage = null;
 }
} 

OK,做的很好了,除非你想做的更好!

2.4、总会有人粗心忘记调用Dispose!

要是有人使用你的类创建了对象,但是忘记调用Dispose方法该怎么办?这将会泄露一些非托管的资源!

注意:忘记调用Dispose方法虽然会造成非托管资源的泄露,但是对于那些托管资源来说,是不会泄露的,因为最终GC会行动起来,在后台线程中释放那些和托管资源有关的内存空间。这包括你创建的对象和其中的托管资源(例如Bitmap和数据库连接) (为什么?往下看)

也就是说,如果你忘记调用Dispose方法,这个类应该自动进行一些补救措施!我们可以想到设计一种方法来做为一种后备方法:利用GC最终调用的终结器

注意:GC最终虽然会释放托管资源,但是GC并不知道或者关心你的Dispose方法。那仅仅是一个我们选择的名字。

GC调用析构函数是一个完美的时机来释放托管资源,我们实现析构函数的功能通过重写Finalize方法。

注意:在C#中,你不能真的去使用重写虚函数的方法去重写Finalize方法。你只能使用像C++的析构函数的语法去编写,编译器会自动对你的代码进行一些改动来实现override终结器方法。(具体可查看MSDN中析构函数一节)

~MyObject()
{
 //we're being finalized (i.e.destroyed), call Dispose in case the user forgot to
 Dispose(); //<--Warning:subtle bug! Keep reading!
} 

但是这有一个Bug。

试想一下,你的GC是在后台线程调用,你对GC的调用时间和对垃圾对象的释放顺序根本无法掌控,很有可能的发生的是,你的对象的某些托管资源已经在调用终结器之前就被释放了,当GC调用终结器时,最终会释放两次托管资源!

public void Dispose()
{
 //Free unmanaged resources
 Win32.DestroyHandle(this.gdiCursorBitmapStreamFileHandle); 

 //Free managed resources too
 if (this.databaseConnection !=null)
 {
 this.databaseConnection.Dispose(); //<-- crash, GC already destroyedit
 this.databaseConnection =null;
 }
 if (this.frameBufferImage !=null)
 {
 this.frameBufferImage.Dispose(); //<-- crash, GC already destroyed it
 this.frameBufferImage = null;
 }
} 

所以你需要做的是让终结器告诉Dispose方法,它不应该再次释放任何托管资源了(因为这些资源很有可能已经被释放了!)

标准的Dispose模式是让Dispose函数和Finalize方法通过调用第三种方法,这个方法有一个bool类型的形参来表示此方法的调用是通过Dispose还是GC调用终结器。在这个方法里实现具体的释放资源代码。

这个第三种方法的命名当然可以随意命名,但是规范的方法签名是:

protected void Dispose(Boolean disposing) 

当然,这个参数的名字会让人读不懂什么意思。(很多网上教程都使用这个名字,第一次看很难看懂这个变量的作用)

这个参数也许可以这样写…

protected void Dispose(Boolean itIsSafeToAlsoFreeManagedObjects)
{
 //Free unmanaged resources
 Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle); 

 //Free managed resources too, butonly if I'm being called from Dispose
 //(If I'm being called fromFinalize then the objects might not exist
 //anymore
 if(itIsSafeToAlsoFreeManagedObjects)
 {
  if (this.databaseConnection !=null)
  {
  this.databaseConnection.Dispose();
   this.databaseConnection =null;
  }
  if (this.frameBufferImage !=null)
  {
  this.frameBufferImage.Dispose();
   this.frameBufferImage =null;
  }
 }
} 

这样IDisposable中的Dispose方法就变成了这样:

public void Dispose()
{
 Dispose(true); //I am calling youfrom Dispose, it's safe
} 

终结器就变成了这样…

~MyObject()
{
 Dispose(false); //I am *not*calling you from Dispose, it's *not* safe
} 

注意:你的类如果是从另一个类继承而来,那么你不要忘记去调用父类的Dispose方法

public Dispose()
{
 try
 {
  Dispose(true); //true: safeto free managed resources
 }
 finally
 {
  base.Dispose();
 }
} 

所有的一切看起来都已经很好了,除非,你想做的更好!

2.5、最完美的方法?

如果使用者手动调用了Dispose方法,这样,一切都被清空了。之后呢,因为你重写了Finalize方法,GC一定调用这个方法,它将会再一次调用Dispose方法!

这不仅是一种性能上的浪费,而且关键是在Dispose方法调用后,那些引用已经变成了垃圾,GC会调用这些垃圾引用!

解决的方法是通过在Dispose方法中调用GC.SuppressFinalize() 来阻止GC去调用Finalize方法

public void Dispose()
{
 Dispose(true); //I am calling youfrom Dispose, it's safe
 GC.SuppressFinalize(this); //Hey,GC: don't bother calling finalize later
} 

这样,每一件事都照顾到了!

(注:其实可以将Dispose(bool disposing)方法变成虚函数,如果你的类被继承)

至此,我们一步一步实现了最好的IDisposable方法,现在回头去看看一开始的实现IDisposable接口教程,是不是一切的透彻了?

三、使用终结器还是Dispose方法释放非托管资源?

其实两种方法都可以,但是就像在一开始提到的,GC的垃圾回收时间不确定,对于那些你已经不需要的资源,还是尽快释放比较好,不应该总等着GC的垃圾回收,而且还有一个好处是,降低GC垃圾回收的时间,提高效率。何乐而不为呢?

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • 轻松学习C#的密封类

    密封类概述        并不是所有的类都可以被继承,C#语言中就是密封类.在C#语言中我们可以把类声明为sealed.这表示不能继承该类,如果想要继承那么编译器肯定会报错.在把类或方法标记为sealed时,最可能出现的情形时:如果要对库,类或自己编写的其他类进行操作,则重写某些功能会导致编译错误.也可以因商业原因把类或方法标记为sealed,以防第三方违反注册协议的方式扩展该类.但是在一般情况下,在把类或方法标记为sealed时要小心,因为这么做会限制它的使用.即使不希望它能继承一个类或重写类

  • .NET/C#如何判断某个类是否是泛型类型或泛型接口的子类型详解

    前言 泛型:通过参数化类型来实现在同一份代码上操作多种数据类型.利用"参数化类型"将类型抽象化,从而实现灵活的复用.在.NET类库中处处都可以看到泛型的身影,尤其是数组和集合中,泛型的存在也大大提高了程序员的开发效率.更重要的是,C#的泛型比C++的模板使用更加安全,并且通过避免装箱和拆箱操作来达到性能提升的目的.因此,我们很有必要掌握并善用这个强大的语言特性. C#泛型特点: 1.如果实例化泛型类型的参数相同,那么JIT编辑器会重复使用该类型,因此C#的动态泛型能力避免了C++静态模

  • C#调用webservice接口的最新方法教程

    前言 Web Service也叫XML Web Service WebService是一种可以接收从Internet或者Intranet上的其它系统中传递过来的请求,轻量级的独立的通讯技术.是:通过SOAP在Web上提供的软件服务,使用WSDL文件进行说明,并通过UDDI进行注册. XML:(Extensible Markup Language)扩展型可标记语言.面向短期的临时数据处理.面向万维网络,是Soap的基础. Soap:(Simple Object Access Protocol)简单

  • C# API中模型与它们的接口设计详解

    关键要点 可变模型应该具备自我验证的能力,并实现验证接口. 在共享对象时(特别是在跨线程共享时),考虑使用不可变模型. 考虑支持MVVM风格UI的单层和多层撤消. 在实现属性变更通知时避免不必要的内存分配. 不要覆盖模型的Equals和GetHashCode方法. 在传统的MVC.MVP.MVVM.Web MVC这些UI模式中,模型是一个公共元素.虽然有很多文章讨论这些架构中的视图和控制器,但几乎无一涉及模型.在本文中,我们将讨论模型本身以及相应的.NET接口. 我想先定义一些术语,这些术语在其

  • C#支付宝新版支付请求接口调用

    本文实例为大家分享了C#支付宝新版支付请求接口调用的具体代码,供大家参考,具体内容如下 因为支付宝已经集成了完整的SDK,所以可以使用SDK直接调用API,这里获取SDK源码. 首先我们需要引用支付宝SDK集成 AopSdk.dll. 添加相关引用: using Aop.Api; using Aop.Api.Domain; using Aop.Api.Request; using Aop.Api.Response; 需要用到商户私钥,支付宝公钥,请求地址等公共参数,所以可以新建一个config文

  • 理解C#编程中的静态类和静态成员以及密封类

    静态类 静态类与非静态类基本相同,但存在一个区别:静态类不能实例化.也就是说,不能使用 new 关键字创建静态类类型的变量.因为没有实例变量,所以要使用类名本身访问静态类的成员.例如,如果名为 UtilityClass 的静态类有一个名为 MethodA 的公共方法,则按下面的示例所示调用该方法: UtilityClass.MethodA(); 对于只对输入参数进行运算而不获取或设置任何内部实例字段的方法集,静态类可以方便地用作这些方法集的容器.例如,在 .NET Framework 类库中,静

  • C# WebApi 接口传参详解

    前言:还记得刚使用WebApi那会儿,被它的传参机制折腾了好久,查阅了半天资料.如今,使用WebApi也有段时间了,今天就记录下API接口传参的一些方式方法,算是一个笔记,也希望能帮初学者少走弯路.本篇针对初初使用WebApi的同学们,比较基础,有兴趣的且看看. 本篇打算通过get.post.put.delete四种请求方式分别谈谈基础类型(包括int/string/datetime等).实体.数组等类型的参数如何传递. 一.get请求 对于取数据,我们使用最多的应该就是get请求了吧.下面通过

  • 详解C# WebApi 接口测试工具:WebApiTestClient

    前言:这两天在整WebApi的服务,由于调用方是Android客户端,Android开发人员也不懂C#语法,API里面的接口也不能直接给他们看,没办法,只有整个详细一点的文档呗.由于接口个数有点多,每个接口都要详细说明接口作用.参数类型.返回值类型等等,写着写着把博主惹毛了,难道这种文档非要自己写不成?难道网上没有这种文档的展示工具吗?带着这两个问题,在网络世界里寻找,网络世界很奇妙,只要你用心,总能找到或多或少的帮助!这不就被博主找到了这个好用的组件:WebApiTestClient.它对于接

  • .NET C#支付宝条码支付接口详解

    支付宝条码支付接口使用,供大家参考,具体内容如下 应用场景实例 收银员使用扫码设备读取用户支付宝钱包"付款码"后,将二维码或条码信息通过本接口上送至支付宝发起支付. SDK下载 支付宝提供3种开发语言的SDK,选择自己的开发语言下载,项目中会有很多示例.本文选择.NET2010版本. 将SDK项目中的AopSdk.dll文件引用到自己的项目中. 支付类代码 简略版 数据需自行获取 public class ToAlipayBLL { private static readonly To

  • C# 开发(创蓝253)手机短信验证码接口的实例

    创蓝253: https://www.253.com/ #region 获取手机验证码(创蓝253) /// <summary> /// 获取手机验证码(创蓝253) /// </summary> /// <param name="phoneno">手机号</param> /// <returns></returns> [AllowAnonymous] public async Task<IHttpActio

  • C#中的应用程序接口介绍及实现,密封类与密封方法

    API  Application Programming Interface 应用程序接口 接口 定义 :指描述可属于任何类或结构的一组相关功能. 接口的成员可以是方法(不能有方法体),属性,事件和索引器,但不能包含常数,字段,运算符,实例构造函数析构函数或类,也不能包括任何种类的静态成员,接口中的成员不允许添加访问修饰符,(默认都是public) 简介: 1. 接口是一个引用类型,通过接口可以实现多重继承. 2. C#中接口的成员不能有new.public.protected.internal

  • C# WebApi 接口返回值不困惑:返回值类型详解

    前言:已经有一个月没写点什么了,感觉心里空落落的.今天再来篇干货,想要学习Webapi的园友们速速动起来,跟着博主一起来学习吧.之前分享过一篇C#进阶系列--WebApi接口传参不再困惑:传参详解,这篇博文内容本身很基础,没想到引起很多园友关注,感谢大家的支持.作为程序猿,我们都知道参数和返回值是编程领域不可分割的两大块,此前分享了下WebApi的传参机制,今天再来看看WebApi里面另一个重要而又基础的知识点:返回值.还是那句话:本篇针对初初使用WebApi的同学们,比较基础,有兴趣的且看看.

随机推荐