释放模式
注意
此内容根据 Pearson Education, Inc. 许可转载自《框架设计指南:可重用 .NET 库的约定、习语和模式第二版》。 该版本于 2008 年出版,并在此后于第三版对该书进行了全面修订。 此页上的一些信息可能已过时。
所有程序在执行过程中都需要一个或多个系统资源,例如内存、系统句柄或数据库连接。 开发人员在使用此类系统资源时必须谨慎,因为获取并使用这些系统资源后必须将其释放。
CLR 提供对自动内存管理的支持。 托管内存(使用 C# 运算符 new
分配的内存)无需显式释放。 它由垃圾回收器 (GC) 自动释放。 这样,开发人员即可无需自行完成释放内存这一繁琐且艰巨的任务,这也是 .NET Framework 可提供前所未有的生产力的主要原因之一。
遗憾的是,托管内存只是众多系统资源类型中的一种。 托管内存以外的资源仍需显式释放,称为非托管资源。 GC 并不专用于管理此类非托管资源,这意味着管理非托管资源的责任由开发人员承担。
CLR 在释放非托管资源方面提供了一些帮助。 System.Object 声明一个虚拟方法 Finalize(也称为终结器),该方法在 GC 回收对象内存之前由 GC 调用,并且可以被重写以释放非托管资源。 重写终结器的类型称为可终结类型。
尽管终结器在某些清理方案中很有效,但终结器有两个明显的缺点:
在 GC 检测到对象符合回收条件时,调用终结器。 这发生在不再需要资源之后的某个不确定时间段。 在需要大量稀缺资源(容易耗尽的资源)的程序中,或者如果资源使用成本高昂(如大型非托管内存缓冲区),开发人员可以或想要释放资源的时间与终结器实际释放资源的时间之间的延迟可能是不可接受的。
CLR 需要调用终结器时,它必须将对象内存的回收推迟到下一轮垃圾回收(终结器在两次回收之间运行)。 这意味着在更长的时间内将不释放对象内存(及其引用的所有对象)。
因此,在务必尽快回收非托管资源的方案中,在使用稀缺资源的方案中,或者在终结的 GC 开销增加是不可接受的高性能方案中,完全依赖终结器可能并不合适。
该框架提供了 System.IDisposable 接口,应实现该接口,以允许开发人员通过手动方式在不需要非托管资源时尽快将其释放。 它还提供了 GC.SuppressFinalize 方法,该方法可以告知 GC 某个对象已被手动释放,不再需要被终结,在这种情况下,可以提前回收对象内存。 实现 IDisposable
接口的类型称为可释放类型。
释放模式旨在将终结器和 IDisposable
接口的使用和实现标准化。
该模式的主要动机是降低实现 Finalize 和 Dispose 方法的复杂性。 之所以复杂,是因为这些方法共享部分但不是所有代码路径(差异将在本章后面描述)。 此外,还有该模式的某些元素的一些历史原因,这些原因与确定性资源管理的语言支持的演变有关。
✓ 务必在含可释放类型实例的类型上实现基本释放模式。 有关基本模式的详细信息,请参阅基本释放模式部分。
如果类型负责其他可释放对象的生命周期,则开发人员也需要一种方法来释放它们。 使用容器的 Dispose
方法是实现这一目标的便捷方式。
✓ 务必实现基本释放模式,并在保存需显式释放且没有终结器的资源的类型上提供终结器。
例如,应在存储非托管内存缓冲区的类型上实现该模式。 可终结类型部分介绍了与实现终结器相关的指南。
✓ 考虑在本身不保存非托管资源或可释放对象但大概率自己子类型保存非托管资源或可释放对象的类上实现基本释放模式。
System.IO.Stream 类就是一个很好的例子。 尽管它是一个不保存任何资源的抽象基类,但它的大多数子类都保存,因此,它实现了这种模式。
基本释放模式
该模式的基本实现涉及实现 System.IDisposable
接口和声明 Dispose(bool)
方法,该方法实现所有资源清理逻辑,供 Dispose
方法与可选终结器共享。
以下示例显示了基本模式的简单实现:
public class DisposableResourceHolder : IDisposable {
private SafeHandle resource; // handle to a resource
public DisposableResourceHolder() {
this.resource = ... // allocates the resource
}
public void Dispose() {
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing) {
if (disposing) {
if (resource!= null) resource.Dispose();
}
}
}
布尔参数 disposing
指示方法是从 IDisposable.Dispose
实现还是从终结器调用的。 Dispose(bool)
实现应在访问其他引用对象(例如上例中的资源字段)之前检查该参数。 只有在从 IDisposable.Dispose
实现调用该方法时(disposing
参数为 true 时)才应访问此类对象。 如果从终结器调用该方法(disposing
为 false),则不应访问其他对象。 原因是对象是以不可预测的顺序终结的,因此对象或其任意依赖项可能已经终结。
此外,本部分适用于基尚未实现释放模式的类。 如果继承自已实现该模式的类,只需重写 Dispose(bool)
方法即可提供额外的资源清理逻辑。
✓ 务必声明 protected virtual void Dispose(bool disposing)
方法,以集中管理所有与释放非托管资源相关的逻辑。
所有资源清理都应在此方法中进行。 该方法是从终结器和 IDisposable.Dispose
方法中调用的。 如果从终结器内部调用该参数,则该参数将为 false。 应使用该参数确保终结期间运行的所有代码都不会访问其他可终结的对象。 下一部分将详细介绍如何实现终结器。
protected virtual void Dispose(bool disposing) {
if (disposing) {
if (resource!= null) resource.Dispose();
}
}
✓ 务必直接依次调用 Dispose(true)
和 GC.SuppressFinalize(this)
来实现 IDisposable
接口。
只有在 Dispose(true)
成功执行时才应调用 SuppressFinalize
。
public void Dispose(){
Dispose(true);
GC.SuppressFinalize(this);
}
X 请勿将无参数的 Dispose
方法设置为虚拟的。
Dispose(bool)
方法应被子类重写。
// bad design
public class DisposableResourceHolder : IDisposable {
public virtual void Dispose() { ... }
protected virtual void Dispose(bool disposing) { ... }
}
// good design
public class DisposableResourceHolder : IDisposable {
public void Dispose() { ... }
protected virtual void Dispose(bool disposing) { ... }
}
X 请勿声明除 Dispose()
和 Dispose(bool)
以外的 Dispose
方法的任何重载。
Dispose
应被视为保留字,以规范此模式并避免实现者、用户和编译器难以理解。 某些语言可能会选择在某些类型上自动实现此模式。
✓ 务必允许多次调用 Dispose(bool)
方法。 该方法可能会在第一次调用后选择不执行任何操作。
public class DisposableResourceHolder : IDisposable {
bool disposed = false;
protected virtual void Dispose(bool disposing) {
if (disposed) return;
// cleanup
...
disposed = true;
}
}
X 避免从 Dispose(bool)
中引发异常,除非是在包含进程已受损(泄漏、共享状态不一致等)的危急情况下。
用户希望对 Dispose
的调用不会引发异常。
如果 Dispose
可能引发异常,则不会执行后续的 finally 块清理逻辑。 若要解决此问题,用户需要将对 Dispose
的所有调用(位于 finally 块内!)包装到 try 块中,这会导致清理处理程序非常复杂。 如果执行 Dispose(bool disposing)
方法,则 disposing 为 false 时绝不会引发异常。 如果在终结器上下文中执行,则这样做将终止进程。
✓ 务必从释放该对象后无法使用的任何成员中引发 ObjectDisposedException。
public class DisposableResourceHolder : IDisposable {
bool disposed = false;
SafeHandle resource; // handle to a resource
public void DoSomething() {
if (disposed) throw new ObjectDisposedException(...);
// now call some native methods using the resource
...
}
protected virtual void Dispose(bool disposing) {
if (disposed) return;
// cleanup
...
disposed = true;
}
}
✓ 除了 Dispose()
之外,如果 close 是该领域的标准术语,请考虑提供方法 Close()
。
这样做时,务必使 Close
实现与 Dispose
相同,并考虑显式实现 IDisposable.Dispose
方法。
public class Stream : IDisposable {
IDisposable.Dispose() {
Close();
}
public void Close() {
Dispose(true);
GC.SuppressFinalize(this);
}
}
可终结类型
可终结类型是通过重写终结器并提供 Dispose(bool)
方法中的终结代码路径来扩展基本释放模式的类型。
众所周知,终结器难以正确实现,主要是因为无法在执行终结器过程中对系统状态做出某些(通常有效的)假设。 应仔细考虑以下指南。
请注意,某些指南不仅适用于 Finalize
方法,还适用于从终结器调用的所有代码。 对于先前定义的基本释放模式,这表示 disposing
参数为 false 时在 Dispose(bool disposing)
内执行的逻辑。
如果基类已经是可终结的并实现了基本释放模式,则不应再次重写 Finalize
。 应重写 Dispose(bool)
方法,以提供额外的资源清理逻辑。
以下代码显示了一个可终结类型的示例:
public class ComplexResourceHolder : IDisposable {
private IntPtr buffer; // unmanaged memory buffer
private SafeHandle resource; // disposable handle to a resource
public ComplexResourceHolder() {
this.buffer = ... // allocates memory
this.resource = ... // allocates the resource
}
protected virtual void Dispose(bool disposing) {
ReleaseBuffer(buffer); // release unmanaged memory
if (disposing) { // release other disposable objects
if (resource!= null) resource.Dispose();
}
}
~ComplexResourceHolder() {
Dispose(false);
}
public void Dispose() {
Dispose(true);
GC.SuppressFinalize(this);
}
}
X 避免将类型设置为可终结的。
仔细考虑你认为需要终结器的任何情况。 从性能和代码复杂性的角度来看,具有终结器的实例会产生实际成本。 尽可能使用 SafeHandle 等资源包装器来封装非托管资源,在这种情况下,终结器变得不必要,因为包装器负责其自身的资源清理。
X 请勿将值类型设置为可终结的。
只有引用类型才被 CLR 终结,因此任何将终结器放置在值类型上的尝试都将被忽略。 C# 和 C++ 编译器强制执行此规则。
✓ 如果类型负责释放没有自己专属终结器的非托管资源,务必将类型设置为可终结的。
实现终结器时,只需调用 Dispose(false)
并将所有资源清理逻辑放置到 Dispose(bool disposing)
方法中。
public class ComplexResourceHolder : IDisposable {
~ComplexResourceHolder() {
Dispose(false);
}
protected virtual void Dispose(bool disposing) {
...
}
}
✓ 务必在每个可终结类型上实现基本释放模式。
这样,该类型的用户可以显式地执行终结器负责的那些资源的确定性清理。
X 请勿访问终结器代码路径中的任何可终结对象,因为这些对象将被终结的风险很大。
例如,引用另一个可终结对象 B 的可终结对象 A 不能在 A 的终结器中可靠地使用 B,反之亦然。 终结器是以随机顺序调用的(缺少关键终结的弱顺序保证)。
此外,请注意,将在应用程序域卸载期间或退出进程期间的某些时间点回收静态变量中存储的对象。 如果 Environment.HasShutdownStarted 返回 true,则访问引用可终结对象的静态变量(或调用可能使用静态变量中存储的值的静态方法)可能不安全。
✓ 务必将 Finalize
方法设置为受保护的。
C#、C++ 和 VB.NET 开发人员无需担心这一点,因为编译器有助于强制执行此指南。
X 请勿使异常脱离终结器逻辑,除非发生系统关键故障。
如果终结器引发异常,CLR 将关闭整个进程(自 .NET Framework 2.0 版起),从而阻止其他终结器执行并阻止以受控方式释放资源。
✓ 考虑创建和使用关键的可终结对象(具有包含 CriticalFinalizerObject 的类型层次结构的类型),以应对终结器绝对必须执行的情况,甚至在强制应用程序域卸载和线程中止时也是如此。
Portions © 2005, 2009 Microsoft Corporation 版权所有。 保留所有权利。
在 Pearson Education, Inc. 授权下,由 Addison-Wesley Professional 作为 Microsoft Windows 开发系列的一部分再版自 Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries, 2nd Edition(Framework 设计准则:可重用 .NET 库的约定、惯例和模式第 2 版),由 Krzysztof Cwalina 和 Brad Abrams 发布于 2008 年 10 月 22 日。