65.9K
CodeProject 正在变化。 阅读更多。
Home

正确实现 IDisposable 和 Dispose 模式

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (142投票s)

Aug 27, 2006

CPOL

21分钟阅读

viewsIcon

785456

解释如何正确实现 IDisposable 接口、Dispose 模式和确定性终结。

引言

在大多数现代编程语言中,内存分配在堆或栈上。分配在栈上的内存存储局部变量、参数、返回值,通常由操作系统管理。分配在堆上的内存由不同的编程语言以不同的方式处理。在 C 和 C++ 中,内存必须手动管理,而在 C# 和 Java 中,内存是自动管理的。

尽管手动内存管理对语言运行时来说更容易,但它增加了程序员的复杂性,可能由于不正确的内存管理和对象生命周期维护而导致错误和编码问题。

自动内存管理被称为垃圾回收。尽管垃圾回收对语言运行时来说更困难,但它降低了程序员的复杂性,并有助于减少手动内存管理导致的错误和编码问题。

.NET 中的垃圾回收

.NET 中的垃圾回收器 (GC) 是 .NET 公共语言运行时 (CLR) 的核心部分,所有 .NET 编程语言都可以使用它。GC 从未旨在管理资源;它旨在管理内存分配,并且在管理直接分配给原生 .NET 对象的内存方面做得非常出色。它并非旨在处理非托管内存和操作系统分配的内存,因此管理这些资源成为开发人员的责任。

在非垃圾回收语言中,一旦对象的生命周期结束,无论是通过局部执行块的完成还是抛出异常,析构函数都会启动并自动释放资源。虽然垃圾回收简化了对象生命周期的管理,但它确实阻止了对象知道何时会被回收,这意味着很难确保该对象持有的资源能被尽早释放。在 GC 回收与对象关联的内存之前,会调用 `Finalize` 方法(如果存在)。此方法不与对象的生命周期绑定,因此 `Finalize` 何时(甚至是否)被调用的时机是未定义的。这就是说 GC 执行非确定性终结的含义。

缺乏确定性终结是垃圾回收语言的一个常见抱怨。处理非托管资源的主要目标是以最有效的方式使用它们。缺乏确定性终结似乎违反了该目标。非确定性终结的问题在于它发生在未来未确定的时间点,通常在达到某些内存耗尽阈值时。如果对象持有昂贵的资源,例如文件句柄或数据库连接,这些资源只在对象被终结时才释放。终结是 CLR 提供的“安全网”,用于帮助确保在出现问题(程序崩溃或编码遗漏)时清理对象。

幸运的是,.NET Framework 提供了许多抽象来隐藏处理非托管资源的复杂性。实际上,许多 CLR 类都代表您使用这些非托管资源,并透明地处理所有资源管理。

CLR 确实通过 `IDisposable` 接口、用于显式清理的 `Dispose` 方法以及在某些情况下用于隐式清理的 `Finalize` 方法提供了实现确定性终结的方法。这通常被称为 Dispose 模式(或 IDisposable 模式)。

正确实现此模式对于确保及时正确清理资源至关重要,也为用户提供了确定性、熟悉的方式来处置这些资源。

IDisposable 和资源清理

`IDisposable` 接口定义如下:

public interface IDisposable
{
   void Dispose();
}

要使用 `IDisposable` 接口,您将像这样声明一个类:

public class MyClass : IDisposable
{
    public void Dispose()
    {
        // Perform any object clean up here.

        // If you are inheriting from another class that
        // also implements IDisposable, don't forget to
        // call base.Dispose() as well.
    }
}

显式清理

在类型拥有资源或拥有拥有资源的类型的所有情况下,您应该通过提供公共 `Dispose` 方法,让用户能够显式释放这些资源。这减轻了对 GC 的一些依赖,并为用户提供了一种确定性地回收资源的方法。

隐式清理

.NET 2.0 中的隐式清理应尽可能通过使用 `SafeHandle` 保护资源来提供。在 .NET 1.0 或 1.1 中,此类别不存在,因此您需要实现一个终结器。在 .NET 2.0 中,由于 `SafeHandle` 类的存在,很少需要实现终结器。如果需要额外的终结(或者您正在使用 .NET 1.0 或 1.1),您可以使用特定语言语法实现受保护的 `Finalize` 方法。运行时将作为 GC 终结过程的一部分,非确定性地为您调用 `Finalize`,为您的对象在其生命周期结束时释放资源提供最后的机会。

如果不必,您真的不想编写终结器。正确编写它们可能非常困难,而且编写一个会使您的类使用起来更昂贵,即使从未调用过终结器。所有实现终结器的对象都**必须**放在由 GC 维护的可终结对象列表(称为终结队列)中。这是自动处理的,但无法避免。当您必须清理资源时,您几乎总是希望在 `Dispose` 方法中提供它,而不是在终结器中。但是,当您确实需要终结器时,您希望它是 `Dispose` 的补充,而不是替代。

语法说明和框架版本差异

`Dispose` 和 `Finalize` 方法服务于非常不同的目的,并且有不同的语言语法来声明它们。

语言 析构函数语法 终结器语法
C# public void Dispose() ~T()
C++ (.NET 2.0) ~T() !T()
C++ (.NET 1.0/1.1) public void Dispose() ~T()
Visual Basic (.NET) Public Sub Dispose() Implements IDisposable.Dispose Protected Overrides Sub Finalize()

Dispose 模式

Dispose 模式由 `IDisposable` 接口定义。

如果您的类是 sealed(Visual Basic 中为 NotInheritable),则无需遵循 Dispose 模式;您应该使用简单方法实现 `Dispose` 和 `Finalize` 方法。您仍然需要遵循关于如何实现 `Dispose` 和 `Finalize` 的规则,只是不需要实现完整的模式。

对于非 sealed 且需要进行资源清理的类,您应该严格遵循 Dispose 模式。该模式旨在确保可靠、可预测的清理,防止临时资源泄漏,并为可处置类提供标准、明确的模式。它还有助于子类正确释放基类资源。

语法注意事项

如果您使用的是 C++ (.NET 2.0),您可以简单地编写常用的析构函数 (~T()),编译器将自动生成所有代码以实现 Dispose 模式。如果您需要编写终结器 (!T()),您应该共享代码,将尽可能多的工作放入终结器中(终结器无法访问其他对象,因此不要在那里放置需要它们的代码),其余部分放入析构函数中,并让析构函数显式调用终结器。

Dispose 模式应在具有可处置子类型的类上实现,即使基类型不拥有任何资源。这有助于使用您的基类的程序员正确处置进一步派生的实例。如果您从已实现 `IDisposable` 的类继承,请勿重新实现它。您只在继承链中的类尚未实现 `IDisposable` 时才需要实现它。

一个很好的例子是 `System.IO.Stream` 类。尽管它是抽象的并且不持有任何资源,但它的子类却持有。因此,它实现了 Dispose 模式以帮助子类。

public abstract class Stream : MarshalByRefObject, IDisposable
{
    public virtual void Close();
    public void Dispose();
    protected virtual void Dispose(bool disposing);
}

// Implicitly gains the Dispose pattern because
// Stream inherits from it, so there is no
// need to re-implement the pattern.
public class MemoryStream : Stream
{
}

// Implicitly gains the Dispose pattern because
// Stream inherits from it, so there is no
// need to re-implement the pattern.
public class FileStream : Stream
{
}

公共 void Dispose() 方法应保持最终状态(换句话说,不要将其设为 virtual 方法),并且应始终如下所示:

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

这两个调用的顺序很重要,不应更改。此顺序确保 `GC.SupressFinalize()` 仅在 `Dispose` 操作成功完成时才被调用。当 `Dispose` 调用 Dispose(true) 时,该调用可能会失败,但当 `Finalize` 稍后被调用时,它会调用 Dispose(false)。实际上,这是两个不同的调用,可以执行代码的不同部分,因此即使 Dispose(true) 失败,Dispose(false) 也可能不会失败。

您的所有资源清理都应包含在 Dispose(bool disposing) 方法中。如有必要,您应通过测试 `disposing` 参数来保护清理。这应适用于托管和非托管资源。Dispose(bool disposing) 在两种不同的情况下运行:

  • 如果 `disposing` 等于 **true**,则该方法已由用户代码直接或间接调用。可以处置托管和非托管资源。
  • 如果 `disposing` 等于 **false**,则该方法已由运行时从终结器内部调用,您不应引用其他对象。只能处置非托管资源。

为了让您的基类有机会清理资源,您应该始终在您自己的 Dispose(bool disposing) 方法中,作为最后一个操作,调用基类的 Dispose(bool disposing)(如果可用)。确保将相同的 `disposing` 值传递给基类的 Dispose(bool disposing) 方法。

protected virtual void Dispose(bool disposing)
{
    if (!disposed)
    {
        if (disposing)
        {
            // Dispose managed resources.
        }

        // There are no unmanaged resources to release, but
        // if we add them, they need to be released here.
    }
    disposed = true;

    // If it is available, make the call to the
    // base class's Dispose(Boolean) method
    base.Dispose(disposing);
}

如果您的对象负责至少一个没有自己的终结器的资源,您应该在您的对象中实现一个终结器。如果您的基类已经重写了 `Finalize` 以遵循此模式,您不应该自己重写它。您的 `Finalize` 方法应该进行一次对 Dispose(false) 的虚拟调用。所有终结清理逻辑都应该在 Dispose(bool disposing) 方法中。

如果您的继承链中的任何基类实现了 `IDisposable` 接口、重写了 void Dispose() 或重写了 `Finalize`,您应该简单地重写 Dispose(bool disposing),添加您的清理逻辑,并确保将 base.Dispose(disposing) 作为最后一条语句调用。

语法注意事项

为了避免混淆,Dispose 应被视为保留字,因此除了以下两种之外,您不应创建任何其他变体:

  • void Dispose()
  • void Dispose(bool disposing)

一些示例

Dispose 模式最简单的实现不包含 `Finalize` 方法。这是您将遵循的大多数类型的模式。

public class SimpleCleanup : IDisposable
{
    // some fields that require cleanup
    private SafeHandle handle;
    private bool disposed = false; // to detect redundant calls

    public SimpleCleanup()
    {
        this.handle = /*...*/;
    }

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

            disposed = true;
        }
    }

    public void Dispose()
    {
        Dispose(true);
    }
}

该模式的一个更复杂的实现是实现 `Finalizer` 的基类实现。`ComplexCleanupExtender` 展示了如何从子类连接到 `Dispose` 和 `Finalize` 循环。请注意,它没有重新实现 `Dispose` 或 `Finalize`。

public class ComplexCleanupBase : IDisposable
{
    // some fields that require cleanup
    private bool disposed = false; // to detect redundant calls

    public ComplexCleanupBase()
    {
        // allocate resources
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                // dispose-only, i.e. non-finalizable logic
            }

            // shared cleanup logic
            disposed = true;
        }
    }

    ~ComplexCleanupBase()
    {
        Dispose(false);
    }

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

public class ComplexCleanupExtender : ComplexCleanupBase
{
    // some new fields that require cleanup
    private bool disposed = false; // to detect redundant calls

    public ComplexCleanupExtender() : base()
    {
        // allocate more resources (in addition to base’s)
    }

    protected override void Dispose(bool disposing)
    {
       if (!disposed)
       {
            if (disposing)
            {
                // dispose-only, i.e. non-finalizable logic
            }

            // new shared cleanup logic
            disposed = true;
        }

        base.Dispose(disposing);
    }
}

实现 Dispose

如果您的类有终结器,您应该始终实现 `Dispose`。这使得您的类的用户能够显式清理终结器负责的资源。您也可以在没有终结器的类上实现 `Dispose`。此规则的唯一例外是值类型,它们不能有终结器或析构函数。

重要的是要记住 `Dispose` 可能会被多次调用。如果发生这种情况,您不应抛出异常,但您可以选择忽略后续调用。

语言警告

C# 自动生成的终结器链确实会多次调用 Dispose(bool),这就是为什么在继承链中只实现一次该模式特别重要。

如果您的对象控制任何可处置类型,它应该在您的 `Dispose` 方法中调用这些类型的 `Dispose`。如果这是从 Dispose(bool disposing) 方法内部完成的,您应该只在 `disposing` 为 **true** 时才这样做。如果您的对象不控制可处置类型,您不应尝试处置它,因为其他代码可能仍在引用它。

public class MyClass : IDisposable
{
   private TextReader reader;
   private bool disposed = false; // to detect redundant calls

   public MyClass()
   {
       this.reader = new TextReader();
   }

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

           disposed = true;
       }
   }

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

语言注意事项

在 C++ 中,您可以遵循按值存储 `TextReader` 的正常模式。析构函数将按照正常的 C++ 语义自动调用 reader.~TextReader()

除了在危急情况下,您不应该从 `Dispose` 中抛出异常。如果执行 Dispose(bool disposing) 方法,则在 `disposing` 为 **false** 时绝不能抛出异常;如果在终结器上下文中执行,这样做将终止进程。

重新创建已处置的对象通常非常困难,并且通常不应这样做。因此,您应该考虑在调用 `Dispose` 后通过抛出 `ObjectDisposedException` 使您的对象不可用。如果您能够重新构造对象,您应该警告用户他们可能需要多次重新处置对象。

如果您的对象的语义允许,请为您的资源清理实现一个 `Close` 方法。此方法的实现应与 `Dispose` 相同,因为大多数开发人员不会想到同时调用 `Close` 和 `Dispose`。在这些情况下,`Dispose` 方法应是对 `Close` 的调用。

public class MyClass : IDisposable
{
   private TextReader reader;
   private bool disposed = false; // to detect redundant calls

   public MyClass()
   {
       this.reader = new TextReader();
   }

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

           disposed = true;
       }
   }

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

   public void Dispose()
   {
       this.Close();
   }
}

然而,有时需要为 `Close` 和 `Dispose` 提供不同的实现。当对象可以多次打开和关闭而无需重新创建实例时,通常会出现这种情况。.NET Framework 在 `System.Data.SqlClient.SqlConnection` 类中使用了这种模式。此类允许您多次打开和关闭连接,但在使用完后仍需要处置实例。

高级实现

作为替代方案,您可以在 `Close` 中释放资源,然后在随后的 `Open` 中延迟重新获取它们。如果您选择这样做,您应该在重新获取它们时调用 GC.ReRegisterForFinalize(this)。您还可能需要重建对象状态。

如果您的对象中存在循环引用,您应该在调用 `Dispose` 之前将这些引用设置为 null

public class CyclicClassA : IDisposable
{
    private TextReader myReader;
    private CyclicClassB cycle;

    public void Dispose()
    {
        if (myReader != null)
        {
            ((IDisposable)myReader).Dispose();
            myReader = null;
        }

        if (cycle != null)
        {
            CyclicClassB b = cycle;
            cycle = null;
            b.Dispose();
        }
    }
}

public class CyclicClassB : IDisposable
{
    private Bitmap bmp;
    private CyclicClassA cycle;

    public void Dispose()
    {
        if (bmp != null)
        {
            bmp.Dispose();
            bmp = null;
        }

        if (cycle != null)
        {
            CyclicClassA a = cycle;
            cycle = null;
            a.Dispose();
        }
    }
}

在此示例中,给定 `CyclicClassA a` 和 `CyclicClassB b` 的实例,如果 `a.cycle = b` 且 `b.cycle = a`,则传递性处置通常会导致无限循环。在上面的示例中,请注意对象的状态首先被置空,以防止此类循环的发生。

在调用 `Dispose` 之前,您还应该考虑将您拥有的任何大型且昂贵的托管对象设置为 null。这很少必要,但它可以通过使对象更快地符合垃圾回收条件来帮助缩短对象的生命周期。当然,大型和昂贵的定义是主观的,应该基于性能分析和测量。

如果您正在创建值类型,您应该避免使其可处置,并且它不应直接包含非托管资源。

实现终结器

终结器很难正确实现,主要是因为在执行过程中,您无法对系统状态做出某些(通常有效)的假设。如果您决定实现终结器,您应该确保正确地执行。实现终结器会带来性能和代码复杂性成本,因此在这样做之前请非常仔细地考虑。在可能的情况下,您应该使用资源包装器,例如 `SafeHandle`,来封装非托管资源。

终结会增加您的对象生命周期的成本和持续时间,因为每个可终结对象在分配时都必须放置在终结队列中。即使从未调用过终结器,也会发生这种情况。这导致 GC 更努力地处置您的对象,并使其存活更长时间。

如果您必须实现终结器,请将 `Finalize` 方法设置为 protected,而不是 publicprivate。对于 C# 和 C++,编译器将自动为您完成此操作。

这些规则不仅适用于 `Finalize` 方法,也适用于在终结期间执行的任何代码。如果您实现本文中描述的 Dispose 模式,它也适用于在 `disposing` 为 **false** 时在 Dispose(bool disposing) 内部执行的代码。

由于终结器是非确定性地运行的,它们之间没有顺序,因此您不能依赖它们以特定顺序执行。您不应访问您的类型可能引用的任何可终结对象,因为它们可能已经终结。因此,您应该只释放您的对象拥有的非托管资源。

实施注意事项

此警告也适用于静态变量。访问引用可终结对象的静态变量是不安全的。这也适用于调用可能使用存储在静态变量中的值的静态方法。在 .NET 1.1 及更高版本中,您可以使用 `Environment.HasShutdownStarted` 来检测您的终结器是否正在运行。访问未装箱的值类型是可以的。

您绝不应直接调用 `Finalize` 方法。在 C# 中,这是不允许的,但在 VB 和 C++ 中是可能的。事实上,它是合法的中间语言 (IL) 语法。尽管您绝不应直接调用 `Finalize`,但它仍应能够处理它**被**多次调用的情况。为此,您可能需要一种方法来检测终结是否已经发生。

终结队列中的其他对象始终可能仍然有对您的对象的实时引用,甚至可能在您的终结器运行之后。您应该能够检测到您的对象在执行任何方法期间是否处于不一致状态。如果您定义了一个关闭您的类型使用的资源的终结器,您可能需要在任何不使用 this 指针(Visual Basic 中为 Me)的实例方法末尾调用 `GC.KeepAlive`,在对该资源执行某些操作之后。这也意味着您需要能够处理部分构造的实例,这可能在构造函数从未完成时发生。如果构造函数抛出异常,`Finalize` 仍将被调用。在这种情况下,某些字段可能尚未初始化。

例如,在以下代码中,如果构造函数在 `list` 被赋值之前抛出异常,`list` 可能为 null

public class MyClass
{
    private ArrayList list;

    public MyClass()
    {
        // do some work that might throw an exception
        list = new ArrayList();
    }

    ~MyClass()
    {
        // list could be null
        foreach(IntPtr i in list)
        {
            CloseHandle(i);
        }
    }
}

如果 `list` 在终结器运行时为 null,它将抛出未处理的 `NullReferenceException`,这将终止程序。要修复此错误,您需要像这样保护终结器:

~MyClass()
{
    // list could be null
    if (list != null)
    {
        foreach(IntPtr i in list)
        {
            CloseHandle(i);
        }
    }
}

终结器不应引发任何未处理的异常,除非在非常关键的系统条件下,例如 `OutOfMemory`。自 .NET 2.0 起,抛出未处理的异常将终止整个进程。

终结器应足够简单,以免失败。由于内存分配可能因内存不足而失败(毕竟,我们正在 GC 的上下文中运行),因此您绝不应在终结器内部分配内存。这在关键终结器或 `SafeHandle` 的 `ReleaseHandle` 方法中尤为重要。关键终结器仅限于调用框架的可靠子集。如果您的关键终结器检测到损坏,或从 Win32 子系统收到错误的错误代码,抛出异常可能是报告此错误的合理方式,但对于 `SafeHandle` 的 `ReleaseHandle`,您应该返回 false

在某些非常罕见的情况下,只有关键终结器会运行。在更罕见的情况下,根本不会运行终结器。您绝不应假设您的终结器总是会运行。处理这种情况的一个好方法是将关键资源包装在一个具有终结器的非公共实例中。这样,任何使用您的类型的人都无法抑制终结。如果您可以迁移到使用 `SafeHandle` 并且从不在类外部公开它,您可以保证资源的终结。

如果您需要一个绝对必须执行的终结器,请考虑使用关键可终结对象。`SafeHandle` 就是这样一个对象。任何继承自 `CriticalFinalizerObject` 的类也是安全的。

除了在非常受控的设计中,例如 Dispose(bool disposing) 方法,您不应在终结器内部调用虚拟成员。将运行虚拟方法的最派生实现,并且不能保证它会像它应该的那样链接到其基类。

终结器可以以任何顺序、在任何线程上运行,可以同时发生在多个对象上,甚至可以同时发生在同一个对象上。通常,不能保证终结的线程策略将来会保持不变,因此您不应依赖于今天的实现方式。您的终结器应该能够在任何线程上运行;也就是说,它应该是线程安全的且线程无关的。

高级信息

有关终结的线程环境的完整描述,您可以查看 Chris Brumme 在 MSDN[^] 上的博客文章。

除了线程无关和线程安全之外,您的终结器还应避免进行任何阻塞调用。您不应执行任何同步或锁定获取、休眠线程或任何其他类似操作,除非存在导致终结失败的真正安全或压力错误。在终结器中阻塞执行可能会延迟甚至阻止队列中的其他终结器运行。如果您必须执行原子线程安全操作,您应该使用 `Interlocked` 类,因为它轻量且非阻塞。

重要的是要记住不要从终结器内部修改线程上下文,因为它最终会污染终结器线程。终结器将在您的对象处于活动状态时所在的线程完全不同的一个单独线程上运行,因此不要让终结器线程模拟,访问线程局部存储,或更改线程的文化。

如果您需要回收(复活)对象,您应该尝试在 `Dispose` 方法中完成。只有在万不得已时才在终结器中这样做。重新注册对象会带来性能影响,并可能导致意外行为。此外,您不应假设通过避免复活对象,您就阻止了它在终结之后(或期间)被访问。其他对象可能会尝试使用您的对象,就好像您仍然活着一样。最好的选择是在这些情况下抛出异常,将其视为类似于访问已处置对象的情况。

值类型不应具有终结器。只有引用类型才由运行时进行终结。

语法注意事项

对于 C# 开发人员,C# 析构函数语法由编译器自动转换为 `Finalize()` 方法。如果您实现 C# 析构函数语法,请确保不要包含对 base.Finalize() 的调用,因为它将为您包含。

例如,以下 C# 类

public class MyClass
{
   public MyClass()
   {
   }

   ~MyClass()
   {
      // object cleanup
   }
}

会自动编译为

public class MyClass
{
   public MyClass()
   {
   }

   protected void Finalize()
   {
      try
      {
         // object cleanup
      }
      finally
      {
         base.Finalize();
      }
   }
}

如果您尝试同时定义析构函数和 `Finalize()` 方法,或者您尝试显式调用基类的 `Finalize()` 方法,编译器将生成错误。编译器将自动确定类层次结构中析构函数的存在,并以正确的顺序(从最低子类到最顶层基类)调用每个析构函数。

使用可处置对象

实现 `IDisposable` 和 Dispose 模式对于您如何编写类非常重要,但并不决定您如何使用类的实例。不幸的是,无法强制您的类的用户在使用完后调用 `Dispose`,也无法强制他们使用适当的异常处理以确保即使抛出异常也调用 `Dispose`。

如果您正在使用的对象实现了 `IDisposable`,或者只是实现了一个公共 `Dispose` 方法,客户端应该正确地限制代码范围,然后在 try/finally 块中处置它持有的资源。如果没有 try/finally 块,如果调用方法导致抛出异常,则永远不会达到客户端对 `Dispose` 的调用。您绝不应忽略从 `Dispose` 调用抛出的异常。

using 语句

C# 和 VB 语言提供了 using 语句,通过在控制离开该范围时自动处置可处置对象,使开发人员更容易使用它们。C++ 通过栈分配语义提供此功能。

例如,以下 C# 代码

public void DoWork()
{
    using (MyClass myClass = new MyClass())
    {
        myClass.SomeMethod();
    }
}

等效于以下 C++ 代码

public void DoWork()
{
    MyClass myClass;
    myClass.SomeMethod();
}

这由编译器自动翻译为

public class Test
{
    public void DoWork()
    {
        MyClass myClass = new MyClass();

        try
        {
            myClass.SomeMethod();
        }
        finally
        {
            if (myClass != null)
            {
                IDisposable disposable = myClass;
                disposable.Dispose();
            }
        }
     }
}

如您所见,using 语句大大简化了代码,这使得它从开发人员的角度来看更具吸引力。

using 语句的语法有许多不同的变体,并且允许您“嵌套”或“堆叠”语句。

以下所有示例都是等效的:

public void DoWork()
{
    using (MyClass myClass = new MyClass(), myClass2 = new MyClass())
    {
        myClass.SomeMethod();
        myClass2.SomeMethod();
    }
}

public void DoWork()
{
    using (MyClass myClass = new MyClass())
    using (MyClass myClass2 = new MyClass())
    {
        myClass.SomeMethod();
        myClass2.SomeMethod();
    }
}

这等效于以下 C++ 代码:

public void DoWork()
{
    MyClass myClass;
    MyClass myClass2;

    myClass.SomeMethod();
    myClass2.SomeMethod();
}

在使用不同的可处置类型时,您还可以堆叠 using 语句。以下示例显示了这一点,并且是等效的:

public void DoWork()
{
    WebRequest request = WebRequest.Create("https://codeproject.org.cn");
    using (WebResponse response = request.GetResponse())
    {
        using (Stream stream = response.GetResponseStream())
        {
            using (StreamReader reader = new StreamReader(stream))
            {
                string result = reader.ReadToEnd().Trim();
            }
        }
    }
 }

public void DoWork()
{
    WebRequest request = WebRequest.Create("https://codeproject.org.cn");
    using (WebResponse response = request.GetResponse())
    using (Stream stream = response.GetResponseStream())
    using (StreamReader reader = new StreamReader(stream))
    {
        string result = reader.ReadToEnd().Trim();
    }
}

接口和 using 语句

如我们所见,using 语句会生成一个类型安全的隐式转换为 `IDisposable`。不幸的是,这阻止了 using 语句与接口一起使用,即使实现类型支持 `IDisposable`。

例如,以下代码将无法编译:

public interface ISomeInterface
{
    void SomeMethod();
}

public class MyClass : ISomeInterface, IDisposable
{
   public void SomeMethod()
   {
   }

   public void Dispose()
   {
   }
}

public class Test
{
    public void DoWork()
    {
        using (ISomeInterface myClass = new MyClass())
        {
            myClass.SomeMethod();
        }
    }
}

一个可能的解决方法是让 `ISomeInterface` 继承自 `IDisposable` 而不是 `MyClass`。这并非在所有情况下都可能,并且可能不合乎要求,具体取决于 `ISomeInterface` 的定义位置及其用途。

一个完整的例子

这是一个在 C# 中实现 Dispose 模式的完整(且稍微复杂一些)示例。

using System;
using System.Security;
using System.ComponentModel;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;

public class ComplexWindow : IDisposable
{
    private MySafeHandleSubclass handle; // pointer for a resource
    private Component component; // other resource you use
    private bool disposed = false;

    public ComplexWindow()
    {
        handle = CreateWindow("MyClass", "Test Window",
            0, 50, 50, 500, 900,
            IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
        component = new Component();
    }

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

    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            // if this is a dispose call dispose on all state you
            // hold, and take yourself off the Finalization queue.
            if (disposing)
            {
                if (handle != null)
                {
                    handle.Dispose();
                }

                if (component != null)
                {
                    component.Dispose();
                    component = null;
                }
            }

            // free your own state (unmanaged objects)
            AdditionalCleanup();

            this.disposed = true;
        }
    }

    // finalizer simply calls Dispose(false)
    ~ComplexWindow()
    {
        Dispose(false);
    }

    // some custom cleanup logic
    private void AdditionalCleanup()
    {
        // this method should not allocate or take locks, unless
        // absolutely needed for security or correctness reasons.
        // since it is called during finalization, it is subject to
        // all of the restrictions on finalizers above.
    }

    // whenever you do something with this class, check to see if the
    // state is disposed, if so, throw this exception.
    public void ShowWindow()
    {
        if (this.disposed)
        {
            throw new ObjectDisposedException("");
        }
        // otherwise, do work
    }

    [DllImport("user32.dll", SetLastError = true,
        CharSet = CharSet.Auto, BestFitMapping = false)]
    private static extern MySafeHandleSubclass CreateWindow(
        string lpClassName, string lpWindowName, int dwStyle,
        int x, int y, int nWidth, int nHeight, IntPtr hwndParent,
        IntPtr Menu, IntPtr hInstance, IntPtr lpParam);

    internal sealed class SafeMyResourceHandle : SafeHandle
    {
        private HandleRef href;

        // called by P/Invoke when returning SafeHandles
        private SafeMyResourceHandle () : base(IntPtr.Zero, true)
        {
        }

        // no need to provide a finalizer - SafeHandle's critical
        // finalizer will call ReleaseHandle for you.
        public override bool IsInvalid
        {
            get
            {
                return handle == IntPtr.Zero;
            }
        }

        override protected bool ReleaseHandle()
        {
            // this method is a constrained execution region, and
            // *must* not allocate
            return DeleteObject(href);
        }

        [DllImport("gdi32.dll", SuppressUnmanagedCodeSecurity]
        [ReliabilityContract(Consistency.WillNotCorruptState,
            CER.Success)]
        private static extern bool DeleteObject(HandleRef hObject);

        [DllImport("kernel32")]
        internal static extern SafeMyResourceHandle CreateHandle(
            int someState);
    }
}

// derived class
public class MyComplexWindow : ComplexWindow
{
    private Component myComponent; // other resource you use
    private bool disposed = false;

    public MyComplexWindow()
    {
        myComponent = new Component();
    }

    protected override void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            if (disposing)
            {
                // free any disposable resources you own
                if (myComponent != null)
                {
                    myComponent.Dispose();
                }
                this.disposed = true;

            }

            // perform any custom clean-up operations
            // such as flushing the stream
        }

        base.Dispose(disposing);
    }
}

结论

如您所见,正确实现 `IDisposable` 和 Dispose 模式涉及许多细节。但是,通过遵循正确的模板和规则,您可以确保您的可处置对象得到正确处理,并在垃圾回收过程中表现得像一等公民。

参考文献与延伸阅读

如需进一步信息,您可以查阅以下参考资料:

修订历史

2006 年 12 月 13 日

  • 添加了指向 Jeffery Richter 的 MSDN 文章的链接。

2006 年 9 月 1 日

  • 根据框架设计指南中提出的指导方针进行了重大重写。
  • 根据评论进行了修订。
  • 修订了参考文献部分。

2006 年 8 月 27 日

  • 原始文章。
© . All rights reserved.