1. 什么是IDisposable?

  IDisposable接口是一个用于约定可进行释放资源操作的接口,一个类实现该接口则意味着可以使用接口约定的方法Dispose来释放资源。其定义如下:
public interface IDisposable
{
void Dispose();
}
  上述描述中可能存在两个问题:
  1. 什么是“资源”?
  2. C#是一个运行在含有垃圾回收(GC)平台上的语言,为什么还需要手动释放资源?

1.1 资源

  资源包括托管资源和非托管资源,具体来说,它可以是一个普通的类实例,是一个数据库连接,是一个文件指针,或者是一个窗口的句柄等等。不太准确地说,你可以理解为就是程序运行时用到的各种东西。

1.2 为什么要手动释放资源

  对于托管资源,通常来说由于有GC的帮助,可以自动释放回收而无需程序员手动管理。然而,由于C#允许使用非托管资源,这些非托管资源不受GC的控制,无法自动释放回收,因此对于这类资源,就要程序员进行手动管理。
(ps:CLR,Common Language Runtime,即C#编译后的IL代码的运行平台)
  如果你写过C++,这就相当于应该在实例销毁时释放掉成“new”出来的分配到堆上的资源,否则资源将一直保留在内存中无法释放,导致内存泄漏等一系列问题。
  在C++中,通常将资源释放的操作放置在类的析构函数中,但C#并没有析构函数这一概念,因此,C#使用IDisposable接口来对资源释放做出约定——当程序员看到一个类实现IDisposable接口时,就应该想到在使用完该类的实例后就应该调用其Dispose方法来及时释放资源。
  对于实现了IDispose接口的类,在C#中你通常可以采用如下方式来释放资源:
  1:try...finally
UnmanagedResource resource = /* ... */;

try
{
// 各种操作
}
finally
{
resource.Dispose();
}

(注:在finally中释放是为了确保即便运行时出错也可以顺利释放资源)

  2:using

using (UnmanagedResource resource = /* ... */)
{
// 离开using的作用域后会自动调用resource的Dispose方法
} // 或者如果不需要额外控制作用域的简写
using UnmanagedResource resource = /* ... */;
(ps:实际上,哪怕不实现IDisposable接口,只要类实现了public void Dispose()方法都可以使用using进行管理)

2. 如何实现IDisposable

2.1 不太完美的基本实现

  你可能还会认为IDisposable很容易实现,毕竟它只有一个方法需要实现,并且看上去只要在方法里释放掉需要释放的资源即可:
class UnmanagedResource : IDisposable
{
public void Dispose()
{
// 释放需要释放的资源
}
}
  通常来说这样做也不会有什么大问题,然而,有几个问题需要考虑。接下来将逐步阐述问题并给出解决方案。

2.2 如果使用者忘记了调用Dispose方法释放资源

  尽管程序员都应该足够细心来保证他们对那些实现了Disposable接口的类的实例调用Dispose方法,但是,出于各种原因,或许是他是一名新手,或许他受到老板的催促,或许他昨天没睡好等等,这些都可能导致他没有仔细检查自己的代码。
永远不要假设你的代码会被一直正确地使用,总得留下些兜底的东西,提高健壮性——把你的用户当做一个做着布朗运动的白痴,哪怕他可能是个经验丰富的程序员,甚至你自己。
  对于这样的问题,最自然的想法自然是交给GC来完成——如果程序员忘记了调用Dispose方法释放资源,就留着让GC来调用释放。还好,C#允许你让GC来帮助你调用一些方法——通过终结器。
  关于终结器的主题会是一个比较复杂的主题,因此在这里不展开讨论,将更多的细节留给其他主题。就本文而言,暂时只需要知道终结器的声明方法以及GC会在“某一时刻”自动调用终结器即可。(你或许想问这个“某一时刻”是什么时候,这实际上是需要交给复杂主题来讨论的话题)
  声明一个终结器类似于声明一个构造方法,但是需要在方法的类名前添加一个~。如下:
class UnmanagedResource : IDisposable
{
// UnmanagedResource的终结器
~UnmanagedResource()
{
// 一些操作
}
}
    关于终结器,下面是一些你需要知道的:
    1:一个类中只能定义一个终结器,且终结器不能有任何访问修饰符(即不能添加public/private/protected/internal)
    2:永远不要手动调用终结器(实际上你也无法这么做)
  由于GC会在某一个时刻自动调用终结器,因此如果在终结器中调用Dispose方法,即使有粗心的程序员忘记了手动释放资源,GC也会在某一时刻来帮他们兜底。如下:
class UnmanagedResource : IDisposable
{
public void Dispose()
{
// 释放需要释放的资源
} ~UnmanagedResource()
{
// 终结器调用Dispose释放资源
Dispose();
}
}

2.3 手动调用了Dispose后,终结器再次调用Dispose

  当你手动调用了Dispose方法后,并不表示你就告诉了GC不要再调用它的终结器,实际上,在你调用Dispose方法后,GC还是会在某一时刻调用终结器,而由于我们在终结器里调用了Dispose方法,这会导致Dispose方法再次被调用——Double Free!
  当然,要解决这一问题非常简单,只需要用一个字段来表明资源是否被释放,并在Dispose方法里检查这个字段的值,一旦发现已经释放则过就立刻返回。如下:
class UnmanagedResource : IDisposable
{
public void Dispose()
{
// 如果已经释放过就立刻返回
if (_disposed)
{
return;
} // 释放需要释放的资源 // 标记已释放
_disposed = true;
} ~UnmanagedResource()
{
Dispose();
} // 用于标记是否已经释放的字段
private bool _disposed;
}
  这样可以解决资源被重复释放的问题,但是这还是无法阻止GC调用终结器。当然你或许会认为让GC调用终结器没什么问题,毕竟我们保证了Dispose重复调用是安全的。不过,要知道终结器是会影响性能的,因此为了性能考虑,我们还是希望在Dispose方法调用后阻止终结器的执行(毕竟这时候已经不需要GC兜底了)。而要实现这一目标十分简单,只需要在Dipose方法中使用GC.SuppressFinalize(this)告诉GC不要调用终结器即可。如下:
class UnmanagedResource : IDisposable
{
public void Dispose()
{
if (_disposed)
{
return;
} // 释放需要释放的资源 _disposed = true; // 告诉GC不要调用当前实例(this)的终结器
GC.SuppressFinalize(this);
} ~UnmanagedResource()
{
Dispose();
} private bool _disposed;
}
  这样,如果调用了Dispose方法,就会“抑制”GC对终结器的调用;而让终结器调用Dispose也不会产生什么问题。

2.4 不是任何时候都需要释放所有资源

  考虑一个比较复杂的类:
class UnmanagedResource : IDisposable
{
// 其他代码 private FileStream _fileStream;
}
  上述例子中,FileStream是一个实现了IDisposable的类,也就是说,FileStream也需要进行释放。UnmanagedResource不仅要释放自己的非托管资源,还要释放FileStream。你或许认为只需要在UnmanagedResourceDispose方法中调用一下FileStreamDispose方法就行。如下:
class UnmanagedResource : IDisposable
{
// 其它代码 public void Dispose()
{
// 其他代码 _fileStream.Dispose(); // 其它代码
} private FileStream _fileStream;
}
  咋一看没什么问题,但是考虑一下,如果UnmanagedResourceDispose方法是由终结器调用的会发生什么?
  提示:终结器的调用是无序的。
  是的,很可能FileStream的终结器先被调用了,执行过了其Dispose方法释放资源,随后UnmanagedResource的终结器调用Dispose方法时会再次调用FileStreamDispose方法——Double Free, Again。
  因此,如果Dispose方法是由终结器调用的,就不应该手动释放那些本身就实现了终结器的托管资源——这些资源的终结器很可能先被执行。仅当手动调用Dispose方法时才手动释放那些实现了终结器的托管资源。
  我们可以使用一个带参数的Dispose方法,用一个参数来指示Dispose是否释放托管资源。稍作调整,实现如下:
class UnmanagedResource : IDisposable
{
// 其它代码
private void Dispose(bool disposing)
{
// 其他代码 if (disposing)
{
// 释放托管资源
_fileStream.Dispose();
} // 释放非托管资源 // 其它代码
}
}
  上述代码声明了一个接受disposing参数的Dispose(bool disposing)方法,当disposingtrue时,同时释放托管资源和非托管资源;当disposingfalse时,仅释放托管资源。另外,为了不公开不必要的接口,将其声明为private
  接下来,只需要在Dispose方法和终结器中按需调用Dispose(bool disposing)方法即可。
class UnmanagedResource : IDisposable
{
// 其它代码 public void Dispose()
{
// disposing=true,手动释放托管资源
Dispose(true);
GC.SuppressFinalize(this);
} ~UnmanagedResource()
{
// disposing=false,不释放托管资源,交给终结器释放
Dispose(false);
} private void Dispose(bool disposing)
{
if (_disposed)
{
return;
} if (disposing)
{
// 释放托管资源
} // 释放非托管资源 _disposed = true;
}
}

2.5 考虑一下子类的资源释放

  考虑一下如果有UnmanagedResource的子类:
class HandleResource : UnmanagedResource
{
private HandlePtr _handlePtr;
}
  HandleResource有自己的资源HandlePtr,显然如果只是简单继承UnmanagedResource的话,UnmanagedResourceDispose方法并不能释放HandleResourceHandlePtr
  那么怎么办呢?使用多态,将UnmanagedResourceDispose方法声明为virtual并在HandleResource里覆写;或者在HandleResource里使用new重新实现Dispose似乎都可以:
// 使用多态
class UnmanagedResource : IDisposable
{
public virtual void Dispose() { /* ... */}
}
class HandleResource : UnmanagedResource
{
public override void Dispose() { /* ... */}
} // 重新实现
class UnmanagedResource : IDisposable
{
public void Dispose() { /* ... */}
}
class HandleResource : UnmanagedResource
{
public new void Dispose() { /* ... */}
}
  这两种方法似乎都可行,但是一个很大的问题是,你还得对HandleResource重复做那些在它的父类UnmanagedResource做过的事——解决重复释放、定义终结器以及区分对待托管和非托管资源。

  这太不“继承了”——显然,有更好的实现方法。

  答案是:将UnmanagedResource的的Dispose(bool disposing)方法访问权限更改为protected,并修饰为virtual,以让子类访问/覆盖:
class UnmanagedResource : IDisposable
{
protected virtual void Dispose(bool disposing) { /* ... */ }
}
  这样,子类可以通过覆写Dispose(bool disposing)来实现自己想要的释放功能:

class UnmanagedResource : IDisposable
{
protected override void Dispose(bool disposing)
{
// 其他代码 base.Dispose(disposing);
}
}
(ps:建议先释放子类资源,再释放父类资源)
  由于Dispose(bool disposing)是虚方法,因此父类UnmanagedResource的终结器和Dispose方法中对Dispose(bool disposing)的调用会受多态的影响,调用到正确的释放方法,故子类可以不必再做那些重复工作。

3. 总结

3.1 代码总览

class UnmanagedResource : IDisposable
{
// 对IDisposable接口的实现
public void Dispose()
{
// 调用Dispose(true),同时释放托管资源与非托管资源
Dispose(true);
// 让GC不要调用终结器
GC.SuppressFinalize(this);
} // UnmanagedResource的终结器
~UnmanagedResource()
{
// 调用Dispose(false),仅释放非托管资源,托管资源交给GC处理
Dispose(false);
} // 释放非托管资源,并可以选择性释放托管资源,且可以让子类覆写的Dispose(bool disposing)方法
protected virtual void Dispose(bool disposing)
{
// 防止重复释放
if (_disposed)
{
return;
} // disposing指示是否是否托管资源
if (disposing)
{
// 释放托管资源
} // 释放非托管资源 // 标记已释放
_disposed = true;
}
}

参考资料/更多资料:

【1】:IDisposable 接口

【2】:实现 Dispose 方法

.NET C#基础(9):资源释放 - 需要介入的资源管理的更多相关文章

  1. 基于webrtc的资源释放问题(二)

    基于webrtc的资源释放问题(二) ——建立连接的过程中意外中断 应用背景: 我们在打电话的时候会不会遇到这种情况?打电话的时候未接通之前挂掉了电话,或者在接通之后建立的连接的过程中挂掉电话? 特别 ...

  2. 基于webrtc的资源释放问题(一)

    基于webrtc的资源释放问题(一) ——重复释放webrtc的相关资源 背景: 视频通讯大都只是作为一个功能存在于各种应用中,比如微信,qq .既然只是应用的一部分,这样就涉及反复的开启和关闭视频通 ...

  3. C#资源释放

    转自:http://www.cnblogs.com/psunny/archive/2009/07/07/1518812.html 深刻理解C#中资源释放 今天我的一个朋友看到我写的那篇<C#中用 ...

  4. TList,TObjectList 使用——资源释放

    TOjectList = Class (Tlist); TOjectList继承Tlist,从名字上看就可以知道它是专门为对象列表制作的,那么他到底丰富了那些功能呢? 首先是 TObject 作为对象 ...

  5. Delphi中关于资源释放(Free,Relealse,FreeAndNil)

    根据日常编程经验,得出一些Delphi中关于资源释放的体会. 假如有对象Obj为TObject类型: 1) Obj.Free直接释放资源后,调用OnDestroy事件,但是没有将Obj指针值置为Nil ...

  6. 深刻理解C#中资源释放

    今天我的一个朋友看到我写的那篇<C#中用AJAX验证用户登录>时,给我指出了点小毛病.就是在用户登录时,如果用户登录失败,在下面这段代码中,都会new出来一个User对象,如果连续登录失败 ...

  7. Unity3d: 资源释放时存储空间不足引发的思考和遇到的问题

    手机游戏第一次启动基本上都会做资源释放的操作,这个时候需要考虑存储空间是否足够,但是Unity没有自带获取设备存储空间大小的 接口,需要调用本地方法分别去android或ios获取,这样挺麻烦的.而且 ...

  8. .net 资源释放(托管资源和非托管资源)

    1.托管资源 像int.float.DateTime等都是托管资源:net中80%的资源都是托管资源: 托管资源的回收通过GC(垃圾回收器)自动释放分配给该对象的内存,但无法预测进行垃圾回收的时间,我 ...

  9. C#资源释放及Dispose、Close和析构方法

    https://www.cnblogs.com/luminji/archive/2011/01/05/1926468.html C#资源释放及Dispose.Close和析构方法   备注:此文的部分 ...

  10. atitit.资源释放机制--attilax总结

    atitit.资源释放机制--attilax总结 1. .全手工, 1 2. 引用计数, 1 2.1. 成本也显而易见. 1 2.2. 循环引用的问题, 2 2.3. 引用计数方式事实上也有经典的卡顿 ...

随机推荐

  1. Python基础 - 运算符优先级

    以下表格列出了从最高到最低优先级的所有运算符: 运算符 描述 ** 指数 (最高优先级) ~ + - 按位翻转, 一元加号和减号 (最后两个的方法名为 +@ 和 -@) * / % // 乘,除,取模 ...

  2. Vagrant 学习笔记:搭建 K8s 集群

    Vagrant学习笔记:搭建K8s集群 通常情况下,我们在使用VMware.VirtualBox这一类虚拟机软件创建虚拟开发环境时,往往需要经历寻找并下载操作系统的安装镜像文件,然后根据该镜像文件启动 ...

  3. KL变换

    covariance 指两个变量的相关性:cov(x, y) =E(x y) - E(x) E(y) cov(x, y) < 0 负相关 cov(x, y) = 0 无关 cov(x, y) & ...

  4. Kotlin协程-那些理不清乱不明的关系

    Kotlin的协程自推出以来,受到了越来越多Android开发者的追捧.另一方面由于它庞大的API,也将相当一部分开发者拒之门外.本篇试图从协程的几个重要概念入手,在复杂API中还原出它本来的面目,以 ...

  5. 【Netty】02-入门

    二. Netty 入门 1. 概述 1.1 Netty 是什么? Netty is an asynchronous event-driven network application framework ...

  6. Selenium:设置元素等待、上传文件、下载文件

    前言:在工作和学习selenium自动化过程中记录学习知识点,深化知识点 1. 设置元素等待 元素定位之元素等待-- WebDriver提供了两种类型的等待:显示等待和隐式等待. 1.1 显示等待 显 ...

  7. Seal AppManager v0.2 发布:进一步简化应用部署体验

    经过近3个月的研发,Seal AppManager v0.2 已正式发布. Seal AppManager 是一款基于平台工程理念的应用统一部署管理平台,于今年4月首次推出.在上一版本中,我们已经释出 ...

  8. Open LLM 排行榜近况

    Open LLM 排行榜是 Hugging Face 设立的一个用于评测开放大语言模型的公开榜单.最近,随着 Falcon 的发布并在 Open LLM 排行榜 上疯狂屠榜,围绕这个榜单在推特上掀起了 ...

  9. Hexo博客Next主题文章置顶相关

    我需要写一些文章做推荐相关,需要文章置顶功能 博客效果 置顶方法配置 一.修改库文件 原理 在Hexo生成首页HTML时,将top值高的文章排在前面,达到置顶功能. 修改方法 修改Hexo文件夹下的n ...

  10. 运维自动化工具--Ansible

    运维自动化工具Ansible 1. ansible安装 rocky安装 需要先安装 enel源 # yum install -y epel-release 然后再安装ansible # yum ins ...