原文链接:
进击的辣条———深入理解C#中的IDisposable接口
https://www.cnblogs.com/wyt007/p/9304564.html
1. 如何正确的显示释放资源。
假设我们要使用FileStream,我们通常的做法就是将其using起来,或者更老是的try…catch…finally…这种做法。因为它的实现调用了非托管资源,所以我们必须用完之后要去显示释放它,如果不去释放它,纳闷可能就造成内存泄漏。
.NET的垃圾回收如何帮我们释放非托管资源,一个标准的释放非托管资源的类应该去实现IDisposable接口:
class MyClass: IDisposable
{
/// <summary>
/// 执行释放或重置非托管资源关联的应用程序定义的任务
/// </summary>
public void Dispose() {
}
}
在我们实例化的时候就可以将这个类using起来:
using (MyClass mc = new MyClass()) {
}
如果要实现IDisposable接口,我们其实应该这样做:
- 实现Dispose方法
- 提取一个受保护的Dispose虚方法,在该方法中实现具体的释放资源的逻辑
- 添加析构函数
- 添加一个私有的bool类型的字段,作为释放资源的标记。
class MyClass: IDisposable
{
/// <summary>
/// 模拟一个非托管资源
/// </summary>
private IntPtr NativeResource { get; set; } = Marshal.AllocHGlobal(100);
/// <summary>
/// 模拟一个托管资源
/// </summary>
private Random ManagedResource { get; set; } = new Random();
//释放标记
private bool disposed;
/// <summary>
/// 为了防止忘记显示调用Dispose方法
/// </summary>
~MyClass(){
//必须为false
Dispose(false);
}
/// <summary>
/// 执行与释放或重置非托管资源关联的应用程序定义的任务
/// </summary>
public void Dispose() {
//必须为true
Dispose(true);
//通知垃圾回收器不再调用终结器
GC.SuppressFinalize(this);
}
/// <summary>
/// 非必须的,只是为了更符合其它语言的规范,如c++,java
/// </summary>
public void Close() {
Dispose();
}
/// <summary>
/// 非密封类可以重写的Dispose方法,方便子类继承时重写
/// </summary>
/// <param name="disposing"></param>
public virtual void Dispose(bool disposing) {
if (disposed) {
return;
}
if (disposing) {
if(ManagedResource != null) {
ManagedResource = null;
}
}
if (NativeResource != IntPtr.Zero) {
Marshal.FreeHGlobal(NativeResource);
NativeResource =IntPtr.Zero ;
}
//告诉自己已经被释放
disposed = true;
}
如果不是虚方法,那么很有可能让开发者在子类继承的时候忽略掉弗雷的清理工作,所以,基于继承体系的原因,我们要提供这样的一个虚方法。
其次,提供的这个虚方法是带bool参数的,带着个参数的目的是为了区释放资源时区分对待托管资源和非托管资源。
而我们的Dispose模式设计思路在于:如果显式调用Dispose,那么类型就该按部就班的将自己的资源全部释放,如果忘记了调用Dispose,那就假定自己的所有资源(哪怕是非普通类型)都交给GC了,所以不需要手动清理,所以这就理解为什么实现自IDisposable的Dispose中调用虚方法是传true,终结器中传false了。
2. 如果对象的某些字段或属性是IDisposable的子类,比如FileStream,那么这个类也应该实现IDisposable
例如:
class MyClass: IDisposable
{
/// <summary>
/// 非托管资源
/// </summary>
private FileStream fs { get; set; }
//释放标记
private bool disposed;
/// <summary>
/// 为了防止忘记显示调用Dispose方法
/// </summary>
~MyClass(){
//必须为false
Dispose(false);
}
/// <summary>
/// 执行与释放或重置非托管资源关联的应用程序定义的任务
/// </summary>
public void Dispose() {
//必须为true
Dispose(true);
//通知垃圾回收器不再调用终结器
GC.SuppressFinalize(this);
}
/// <summary>
/// 非密封类可以重写的Dispose方法,方便子类继承时重写
/// </summary>
/// <param name="disposing"></param>
public virtual void Dispose(bool disposing) {
if (disposed) {
return;
}
if (disposing) {
}
if (fs != null) {
fs.Dispose();
fs=null ;
}
//告诉自己已经被释放
disposed = true;
}
}
3. 及时释放资源
可能很多人会问,GC已经帮我们隐式的释放了资源,为什么还要主动的释放资源,我们举一个例子:
private void button1_Click(object sender, EventArgs e) {
FileStream fs = new FileStream(@"D:\1.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite);
}
private void button2_Click(object sender, EventArgs e) {
GC.Collect();
}
上面的代码在Winform程序中,单击按钮1,打开一个文件流,单击按钮2执行GC回收所有代的垃圾,如果连续单击按钮1,将会抛异常。
如果单击按钮1再单击按钮2,然后再单击按钮1就不会出现这个问题。
原因是:再单击按钮1的时候打开一个文件,方法已经执行完毕,fs已经没有被任何地方引用,所以被标记为了垃圾。那什么时候被回收呢,或者GC什么时候开始工作呢?微软官方给的解释是,当满足以下条件之一是,GC才会工作:
- 系统具有较低的物理内存
- 由托管堆上已分配的对象使用的内存超出了可接受的范围
- 手动调用GC.Collect方法,但几乎所有的情况下, 我们都不必调用,因为垃圾回收器会自动调用它,上面的例子只是为了体验不及时回收垃圾所带来的维护,所以手动调用了GC.Collect
不及时释放资源还会带来另外一个问题,虽然之前我们说试下IDisposable接口的类,GC可以帮我们自动释放,但是这个过程被延迟了,因为他不是在一次回收中完成所有的清理工作。即使GC自动帮我么释放了,那也是先调用FileStream的终结器,在下一次的垃圾回收时才真正被释放。
故,在编码过程中,如果我们明知道它要应该被using起来时,一定要使用using
4. 需不需要将不再使用的对象置为null
有人认为将对象置为null能让GC更早的发现垃圾,也有人认为并没有什么用。接下来为了更好的说明问题,我们先来段代码检验一下:
private void button1_Click(object sender, EventArgs e) {
MyClass mc1 = new MyClass() { Name = "mc1" };
MyClass mc2 = new MyClass() { Name = "mc2" };
mc1 = null;
}
private void button2_Click(object sender, EventArgs e) {
GC.Collect();
}
class MyClass
{
public string Name { get; set; }
~MyClass() {
MessageBox.Show(Name + "被销毁了。");
}
}
单击按钮1,再单击按钮2,我们发现:
没有置为null的mc2会先释放。
其实.NET的JIT编译器是一个优化过的编译器,所以如果我们代码里面将局部变量置为null,这样的语句会被忽略掉:
s=null;
如果我们的项目是在Release配置下的,上面的代码压根就不会被编译到dll,正是由于我们上面的分析,所以很多人都会认为将对象赋值为null完全没有必要,但是,在另一种情况下,就完全有必要将对象赋值为null,那就是静态字段或属性,但这并不意味着将对象赋值为null就是将它的静态字段赋值为null:
private void button6_Click(object sender, EventArgs e)
{
var mc1 = new MyClass() { Name = "mc1" };
var mc2 = new MyClass() { Name = "mc2" };
mc1 = null;
}
private void button7_Click(object sender, EventArgs e)
{
GC.Collect();
}
public class MyClass
{
public string Name { get; set; }
~MyClass()
{
MessageBox.Show(Name + "被销毁了");
}
}
上面的代码运行我们会发现,当mc被回收时,它的静态属性并没有被GC回收,而我们将MyClass终结器中的MyClass2=null的注释取消,再运行,当我们两次点击按钮7的时候,属性MyClass2才被真正的释放,因为第一次GC的时候只是在终结器里面将MyClass属性置为null,在第二次GC的时候才当作垃圾回收了,之所以静态变量不被释放(即使赋值为null也不会被编译器优化),是因为类型的静态字段一旦被创建,就被作为“根”存在,基本上不参与GC,所以GC始终不会认为它是个垃圾,而非静态字段则不会有这样的问题。
所以在实际工作当中,一旦我们感觉静态变量所占用的内存空间较大的时候,并且不会再使用,便可以将其置为null,最典型的案例就是缓存的过期策略的实现了,将静态变量置为null这或许不是很有必要,但这绝对是一个好的习惯,试想一个项目中,如果将某个静态变量作为全局的缓存,如果没有做过期策略,一旦项目运行,那么它所占的内存空间只增不减,最终顶爆机器内存,所以,有个建议就是:尽量地少用静态变量。