《Effective C#》读书笔记——条目17:实现标准的销毁模式<.NET资源管理>
如何为我们自己的包含非托管资源的类型编写资源管理代码呢?在 .NET 中为我们提供了一种标准的销毁非托管资源的模式,这个标准的模式能够使使用者通过调用IDisposable接口正常释放掉非托管资源,也能够保证使用者在忘记释放资源时使用终结器释放。这个标准模式可以和GC配合,保证仅在最糟糕的情况下才调用终结器,尽可能的降低其带来的性能影响。
阅读目录:
1.实现IDisposable接口
实现IDisposable接口是一种标准的做法,用来通知使用者和运行时系统该对象包含的资源需要及时释放。IDisposable.Dispose()方法仅仅定义了一个方法:
1 public interface IDisposable 2 { 3 void Dispose(); 4 }
实现IDisposable.Dispose()方法需要完成以下目标:
- 释放掉所有非托管资源
- 释放掉所有托管资源,包括释放事件监听程序
- 设定一个状态标志,表示该对象已经被销毁
- 跳过终结操作,调用GC.SuppressFinalize(this)即可。
实现IDisposable接口应该完成两件事:
- 提供一种机制,让使用者可以在垃圾收集的时候及时释放掉所有的托管资源
- 提供一种标准做法,让使用者可以释放掉所有的非托管资源(避免终结过程带来的开销)
1.1 资源释放的标准模式
不过这里存在着问题:如何让派生类清理自己的资源,同样也能让基类进行清理呢?如果派生类覆写了终结器,或是实现了IDisposable接口,那么这些方法必须调用基类。否则,基类将不能够被正确清理。在这里我们有一种标准的做法就是:编写一个受保护的虚辅助方法,将销毁和析构共同的工作提取出来,并让派生类也可以释放其自己的资源。基类包含了核心接口的代码,而虚方法则为派生类提供了根据Dispose()或终结器的需要进行资源清理的入口:
1 //Dispose 虚方法 2 //将销毁和析构共同的工作提取出来,并让派生类也可以释放其自己的资源 3 protected virtual void Dispose(bool isDisposing)
该重载方法需要同时支持终结器和Dispose方法,同时因为它是个虚方法所以所有得派生类都可以讲其作为释放资源的入口点。派生类可覆写该方法,并在其中清理自身的资源,然后调用基类的版本。我们来看这一个标准模式的示例代码:
1 public class MyResourceHog : IDisposable 2 { 3 //标记为已销毁 4 private bool alreadyDisposed = false; 5 6 //实现IDisposable 7 //调用定义的Dispose()虚方法 8 //跳过终结器 9 public void Dispose() 10 { 11 Dispose(true); 12 GC.SuppressFinalize(this); 13 } 14 15 //Dispose 虚方法 16 //将销毁和析构共同的工作提取出来,并让派生类也可以释放其自己的资源 17 //isDisposing == true 时,同时清理托管资源; 18 protected virtual void Dispose(bool isDisposing) 19 { 20 //不需要处理多次 21 if (alreadyDisposed) 22 return; 23 if (isDisposing) 24 { 25 //省略:在这里释放托管资源 26 } 27 //省略:在这里释放非托管资源 28 //设置已处理标志 29 alreadyDisposed = true; 30 } 31 32 public void ExampleMethod() 33 { 34 if (alreadyDisposed) 35 throw new ObjectDisposedException("MyResourceHog", "调用了已经被释放的对象"); 36 //省略 37 } 38 }
派生类在执行自己分配的资源清理工作时,可以覆写基类中受保护的Dispose(bool)方法,且无论isDisposing取值如何,都要调用基类的Dispose(isDisposing)方法,以便让基类完成自身资源的释放:
1 public class DerivedResourceHog : MyResourceHog 2 { 3 private bool disposed = false; 4 5 protected override void Dispose(bool isDisposing) 6 { 7 if (disposed) 8 return; 9 if (isDisposing) 10 { 11 //这里释放托管资源 12 } 13 //释放非托管资源 14 15 //这里释放基类资源 16 //基类负责调用 17 // GC.SuppressFinalize(this); 18 base.Dispose(isDisposing); 19 20 //设置已经被销毁的标志 21 disposed = true; 22 } 23 }
我们可以观察到前面的示例中基类和派生类都包含了一个标志,表示对象当前的销毁状态。这是种防御性手段,各个对象维持自身的状态可以把销毁过程中可能出现的错误限制在了一个类型中,而不会影响到组成对象的所有类型。Dispose()方法可以被调用多次,即使对象已经被销毁,终结器也有类似的规则。
同时我们应该看到,示例程序中的两个类并没有提供终结器,这是由于这里没有使用非托管资源——因此不需要终结器(也就是说,上面的代码一直会调用Dispose(true))。除非你的类中包含非托管资源,否则不应该实现终结器,因为这个会对性能造成很大的影响(即使终结器用于也不会被调用)。不过这个标准模式确实不可改变的,因为派生类中可能会使用非托管资源,所以添加终结器,进而实现Dispose(bool),以便正确处理非托管资源。
关于销毁/清理方法最重要的建议:
- Dispose()方法只能释放资源,不能再方法内执行任何别的操作
- 终结器除了清理非托管资源之外不应该有任何别的操作
2.提供终结器
如果你的类使用了非托管资源,那么你必须提供一个终结器。因为类的使用者可能会忘记调用Dispose()方法。如果没有提供终结器,而使用者又忘记调用Dispose()的话,那么就会发生资源泄露。终结器是唯一可以保证能够释放掉非托管资源的方式,没有之一。
我们通过调用Object.Finalize 方法来使用终结器,默认情况下,Finalize方法不会执行任何操作,如果我们想要让GC在回收对象内存前执行清理非托管资源的操作,我们必须先在类中重写该方法(添加析构函数)。
2.1 析构函数
在C#中不能够直接重写或调用Finalize(),只能通过析构函数语法来间接调用终结器。
析构函数是C#调用终结器的操作机制,析构函数是由GC来负责调用的。程序退出时也会调用析构函数。析构函数具有下面的几个特点:
- 只能对类使用析构函数(结构不可以)。
- 一个类只能有一个析构函数。
- 无法继承或重载析构函数。
- 无法调用析构函数。 它们是被自动调用的。
- 析构函数既没有修饰符,也没有参数。
我们看下面的使用析构函数的示例:
1 public class Employee 2 { 3 //析构函数 4 ~Employee() 5 { 6 //清理操作 7 } 8 }
经过编译器编译后会生成和下面类似的代码:
1 public class Employee 2 { 3 protected override void Finalize() 4 { 5 try 6 { 7 //清理操作... 8 } 9 finally 10 { 11 base.Finalize(); 12 } 13 } 14 }
从上面的代码我们知道:通过自动调用基类型的析构函数可以保证继承链上的对象所使用的非托管资源得到有效的释放。或者我们可以直接查看IL代码:
在GC运行时,它会立即清理掉那些没有提供终结器的垃圾对象,而提供了终结器的垃圾对象会停留在内存中,被添加到一个叫做“终结队列“(finalization queue)的地方。GC会使用另一个线程来执行队列中对象的终结。终结器完成工作之后,这些垃圾对象才能够从内存中清理出去。从这里我们可以看出使用终结器会在很大程度上影响程序的性能。
小节
对于包含了非托管资源或者某个成员实现了IDisposable接口的类型必须为其提供一个终结器,即使需要的只是IDisposable接口,而不是终结器也需要实现完整的模式——同时提供终结器和实现IDisposable接口。否则派生类(可能包含非托管资源)就不得不在标志的Dispose模式之外自成体系,增加其复杂性,请遵守前面实现的标准Dispose模式,会节省你、你的类的使用者以及基于你的类型的派生类作者的大量时间。
参考&进一步阅读