处置模式

注释

此内容由 Pearson Education, Inc. 的许可从 框架设计指南:可重用 .NET 库的约定、习惯和模式(第 2 版)重新打印。 该版于2008年出版,此后该书已于 第三版全面修订。 此页上的一些信息可能已过期。

所有程序在执行过程中获取一个或多个系统资源,例如内存、系统句柄或数据库连接。 开发人员在使用此类系统资源时必须小心,因为它们必须在获取和使用后发布。

CLR 支持自动内存管理。 不需要显式释放托管内存(使用 C# 运算符 new分配的内存)。 垃圾回收器(GC)会自动释放它。 这样,开发人员就摆脱了释放内存的繁琐而困难的任务,并且是 .NET Framework 提供前所未有的生产力的主要原因之一。

遗憾的是,托管内存只是许多类型的系统资源之一。 除托管内存以外的资源仍需显式释放,称为非托管资源。 GC 并非专门用于管理此类非托管资源,这意味着管理非托管资源的责任在于开发人员的手中。

CLR 在释放非托管资源方面提供了一些帮助。 System.Object 声明了一种称为终结器的虚拟方法 Finalize,该方法由 GC 在回收对象内存之前调用,并且可以重写以释放非托管资源。 重写终结器的类型称为可终结类型。

尽管终结器在某些清理方案中有效,但它们有两个显著缺点:

  • 当 GC 检测到某个对象符合收集条件时,将调用终结器。 这在不再需要资源之后的某个不确定时间段内发生。 开发人员希望或计划释放资源的时间与终结器实际释放资源的时间之间的延迟,在需要获取许多稀缺资源(容易耗尽的资源)或资源成本高昂的情况下(例如大型非托管内存缓冲区),可能是不可接受的。

  • 当 CLR 需要调用终结器时,它必须将对象的内存回收推迟到下一轮垃圾回收(终结器在回收之间运行)。 这意味着该对象的内存(及其引用的所有对象)在较长时间内不会释放。

因此,在许多情况下,仅依赖终结器可能不合适,尤其是在需要尽快回收非托管资源或处理稀缺资源的场景中,或者在高性能场景中添加的 GC 开销是不可接受的。

框架提供 System.IDisposable 应实现的接口,以便为开发人员提供一种手动方式,以便在不需要资源时立即释放非托管资源。 它还提供了 GC.SuppressFinalize 方法,可以通知 GC,该对象是手动释放的,不再需要进行结束处理,这样的话,可以更早回收对象的内存。 实现 IDisposable 接口的类型称为可释放类型。

释放模式旨在标准化终结器的使用和 IDisposable 接口的实现。

模式的主要动机是降低实现 FinalizeDispose 方法的复杂性。 复杂性源于方法共享部分但并非所有代码路径(本章稍后将介绍差异)这一事实。 此外,某些与语言对确定性资源管理支持演变有关的模式元素,存在其历史原因。

对包含可释放类型实例的类型请实现基本释放模式。 有关基本模式的详细信息,请参阅 “基本释放模式 ”部分。

如果一个类型负责管理其他可释放对象的生命周期,开发人员也需要一种方法来处置这些对象。 使用容器 Dispose 的方法是一种方便的方法,使这一点成为可能。

• DO 实施基本释放模式,并为保存资源的、需要显式释放且没有终结器的类型提供终结器。

例如,应在存储非托管内存缓冲区的类型上实现该模式。 “ 可终结类型” 部分讨论了与实现终结器相关的准则。

• 考虑 在本身不持有非托管资源或可释放对象的类上实现基本释放模式,但这些类可能具有持有非托管资源或可释放对象的子类型。

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 实现中调用该方法,并且参数等于 true 时,才应访问此类对象。 如果从终结器调用该方法(disposing 为 false),则不应访问其他对象。 原因是对象按不可预知的顺序完成,因此它们或其任何依赖项可能已经敲定。

此外,本部分适用于具有尚未实现 Dispose 模式的基础的类。 如果要从已实现模式的类继承,只需重写 Dispose(bool) 方法以提供其他资源清理逻辑。

• DO 声明一种方法 protected virtual void Dispose(bool disposing) 来集中与释放非托管资源相关的所有逻辑。

此方法中应进行所有资源清理。 该方法同时从终结器和 IDisposable.Dispose 方法调用。 如果从终结器内部调用参数,则参数为 false。 它应该用于确保在最终确定期间运行的任何代码都无法访问其他可完成的对象。 下一部分描述了实现终结器的详细信息。

protected virtual void Dispose(bool disposing) {
    if (disposing) {
        if (resource!= null) resource.Dispose();
    }
}

GC.SuppressFinalize(this)

仅当SuppressFinalize成功执行时,才应调用Dispose(true)

public void Dispose(){
    Dispose(true);
    GC.SuppressFinalize(this);
}

X DO NOT 将无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 DO NOT 声明除DisposeDispose()之外的Dispose(bool)方法的任何重载。

Dispose 应被视为保留字,以帮助编撰此模式,并防止实现者、用户和编译器之间的混淆。 某些语言可能会选择在特定类型上自动实现此模式。

✓ 请允许Dispose(bool)方法被多次调用。 该方法可能会在第一次调用后选择不执行任何操作。

public class DisposableResourceHolder : IDisposable {

    bool disposed = false;

    protected virtual void Dispose(bool disposing) {
        if (disposed) return;
        // cleanup
        ...
        disposed = true;
    }
}

X AVOID 避免在 Dispose(bool) 内部抛出异常,除非是在关键情况下例如进程损坏的时候(比如泄漏、不一致的共享状态等)。

用户期望调用 Dispose 不会引发异常。

如果 Dispose 可能会引发异常,则不会执行进一步的 finally 块清理逻辑。 为了规避这个问题,用户需要在 try 块中封装对 Dispose 的每一次调用(尤其是在 finally 块中!),这会导致清理处理程序变得非常复杂。 如果执行 Dispose(bool disposing) 方法,则如果释放为 false,则永远不会引发异常。 这样做会在终结器上下文中执行时终止进程。

• DO 从释放对象后不能使用的任何成员引发 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;
    }
}

• 考虑 提供方法 Close()(如果 Dispose()关闭是区域中的标准术语)。

执行此作时,请务必使 Close 实现与 Dispose 显式实现完全相同,并考虑显式实现 IDisposable.Dispose 方法。

public class Stream : IDisposable {
    IDisposable.Dispose() {
        Close();
    }
    public void Close() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

可终结类型

可终结类型是那些通过重写终结器,并在 Dispose(bool) 方法中提供终结代码路径,以扩展基本释放模式的类型。

终结器众所周知难以正确实现,主要是因为在它们执行期间,你无法对系统状态做出某些通常有效的假设。 应仔细考虑以下准则。

请注意,某些准则不仅适用于Finalize方法,也适用于从终结器调用的任何代码。 对于先前定义的基本释放模式,这意味着逻辑将在Dispose(bool disposing)参数为 false 时于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 DO NOT 允许异常从终结器逻辑中抛出,但系统级关键故障除外。

如果从终结器引发异常,CLR 将终止整个进程(从 .NET Framework 2.0 版本开始),防止其他终结器执行及资源以受控方式释放。

• 考虑 创建和使用关键的可终结对象(类型层次结构包含 CriticalFinalizerObject的类型),在这种情况下,即使面对强制应用程序域卸载和线程中止,终结器也必须执行。

部分内容 © 2005, 2009 Microsoft 公司。 保留所有权利。

经皮尔逊教育有限公司许可,从由 Krzysztof Cwalina 和 Brad Abrams 撰写的《框架设计准则:可重用 .NET 库的约定、习惯和模式》一书中重新印刷。此书由 Addison-Wesley Professional 于 2008 年 10 月 22 日出版,是微软 Windows 开发系列的一部分。

另请参阅