IDisposable接口的正确使用方法

1880

从阅读Microsoft文档了解到,IDisposable接口的“主要”作用是清理非托管资源。

对我来说,“非托管资源”意味着诸如数据库连接、套接字、窗口句柄等东西。但是,我看到过实现Dispose()方法以释放托管资源的代码,这对我来说似乎是多余的,因为垃圾回收器应该已经为你处理了这些。

例如:

public class MyCollection : IDisposable
{
    private List<String> _theList = new List<String>();
    private Dictionary<String, Point> _theDict = new Dictionary<String, Point>();

    // Die, clear it up! (free unmanaged resources)
    public void Dispose()
    {
        _theList.clear();
        _theDict.clear();
        _theList = null;
        _theDict = null;
    }
}

我的问题是,这样做是否会使垃圾回收器释放MyCollection使用的内存比通常情况下更快?


编辑:到目前为止,人们已经发布了一些使用IDisposable清理非托管资源(如数据库连接和位图)的好例子。但是假设上述代码中的_theList包含一百万个字符串,并且您想要立即释放该内存,而不是等待垃圾回收器。那么上面的代码能够实现吗?


43
我喜欢被接受的答案,因为它告诉你如何正确地使用IDisposable“模式”,但正如楼主在他的编辑中所说,它没有回答他想问的问题。IDisposable并不会“调用”垃圾收集器,它只是将一个对象标记为可销毁的。但是,有什么真正的方法可以立即释放内存,而不是等待垃圾收集器启动?我认为这个问题值得更多的讨论。 - Punit Vora
53
IDisposable 并不会标记任何东西。 Dispose 方法会执行必要的操作,以清理实例使用的资源。这与垃圾回收无关。 - John Saunders
5
@John。我了解IDisposable。这就是为什么我说被接受的答案没有回答OP想要问的问题(以及后续编辑),即IDisposable是否有助于<i>释放内存</i>。由于IDisposable与释放资源而不是释放内存无关,因此像你所说的,根本没有必要将托管引用设置为null,这正是OP在他的示例中所做的。因此,对他的问题的正确答案是“不,它并不能更快地释放内存,事实上,它根本不能释放内存,只能释放资源”。但无论如何,感谢您的建议。 - Punit Vora
13
如果是这种情况,那么你就不应该说“IDisposable并不会‘调用’垃圾回收器,它只是将对象‘标记’为可销毁的”。 - John Saunders
7
没有绝对可靠的方法可以确定性地释放内存。你可以调用 GC.Collect(),但那只是个礼貌性的请求,而非要求。所有正在运行的线程必须被暂停才能进行垃圾回收-如果您想了解更多,请阅读 .NET 安全点的概念,例如 https://msdn.microsoft.com/zh-cn/library/678ysw69(v=vs.110).aspx 。如果一个线程无法被挂起,例如因为存在对非托管代码的调用,GC.Collect() 可能根本不会有任何作用。 - Concrete Gannet
显示剩余5条评论
20个回答

2934
Dispose 的目的是释放非托管资源。必须在某个时候完成,否则它们将永远不会被清理。垃圾回收器不知道如何在类型为 IntPtr 的变量上调用 DeleteHandle(),它不知道是否需要调用 DeleteHandle()。
注意:什么是非托管资源?如果您在 Microsoft .NET Framework 中找到它:它是托管的。如果您自己在 MSDN 上查找,它就是非托管的。您使用 P/Invoke 调用获取的任何超出 .NET Framework 提供的所有内容的舒适世界的东西都是非托管的 - 现在您负责清理它。
您创建的对象需要公开一些方法,以便外部世界可以调用它来清理非托管资源。该方法可以命名为任何您喜欢的名称:
public void Cleanup()

或者

public void Shutdown()

但是实际上,这种方法有一个标准化的名称:
public void Dispose()

甚至创建了一个接口,IDisposable,它只有一个方法:

public interface IDisposable
{
   void Dispose();
}

所以你让你的对象暴露出IDisposable接口,这样你承诺你已经编写了单个方法来清理你的非托管资源:

public void Dispose()
{
   Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);
}

完成了。 但你可以做得更好。


如果你的对象已经分配了一个250MB的System.Drawing.Bitmap(即.NET管理的Bitmap类)作为某种帧缓冲区,该怎么办?当然,这是一个托管的.NET对象,垃圾收集器将会释放它。但是,你真的想让250MB的内存闲置等待垃圾回收器最终来释放它吗?如果有一个打开的数据库连接,我们肯定不想让该连接保持打开状态,等待GC来完成对象的操作。
如果用户已经调用了Dispose()(这意味着他们不再计划使用该对象),为什么不摆脱那些浪费的位图和数据库连接呢?
因此,现在我们将:
  • 摆脱非托管资源(因为我们必须这样做),并且
  • 摆脱托管资源(因为我们想要有所帮助)
所以让我们更新我们的Dispose()方法来摆脱这些托管对象:
public void Dispose()
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);

   //Free managed resources too
   if (this.databaseConnection != null)
   {
      this.databaseConnection.Dispose();
      this.databaseConnection = null;
   }
   if (this.frameBufferImage != null)
   {
      this.frameBufferImage.Dispose();
      this.frameBufferImage = null;
   }
}

一切都很好,但你可以做得更好


如果一个人忘记在对象上调用Dispose()会怎么样?那么他们就会泄漏一些非托管资源!
注意:它们不会泄漏托管资源,因为最终垃圾收集器会在后台线程上运行,并释放与任何未使用对象相关联的内存。这将包括您的对象和您使用的任何托管对象(如Bitmap和DbConnection)。
如果一个人忘记调用Dispose(),我们仍然可以救他们!我们仍然有一种方法来代替他们调用它:当垃圾收集器最终开始释放(即完成)我们的对象时。
注意:垃圾收集器最终会释放所有托管对象。当它这样做时,它会调用对象的Finalize方法。GC不知道也不关心您的Dispose方法。那只是我们选择的一个方法名,当我们想要摆脱非托管东西时调用它。

垃圾回收器销毁我们的对象是释放那些讨厌的非托管资源的完美时机。我们通过重写Finalize()方法来实现这一点。

注意:在C#中,您不需要显式地重写Finalize()方法。 您编写一个类似于C++析构函数的方法,编译器会将其视为您对Finalize()方法的实现:

~MyObject()
{
    //we're being finalized (i.e. destroyed), call Dispose in case the user forgot to
    Dispose(); //<--Warning: subtle bug! Keep reading!
}

但是这段代码中有一个错误。你看,垃圾回收器在一个后台线程上运行;你不知道两个对象被销毁的顺序。完全有可能在你的Dispose()代码中,你想要处理的托管对象已经不存在了(因为你想要帮忙清理):

public void Dispose()
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.gdiCursorBitmapStreamFileHandle);

   //Free managed resources too
   if (this.databaseConnection != null)
   {
      this.databaseConnection.Dispose(); //<-- crash, GC already destroyed it
      this.databaseConnection = null;
   }
   if (this.frameBufferImage != null)
   {
      this.frameBufferImage.Dispose(); //<-- crash, GC already destroyed it
      this.frameBufferImage = null;
   }
}

所以你需要的是一种方法,让Finalize()告诉Dispose()它不应该触及任何托管资源(因为它们可能已经不存在了),同时释放非托管资源。

标准模式是让Finalize()Dispose()都调用一个第三个方法;在这个方法中,你传递一个布尔值,表示你是从Dispose()(而不是Finalize())调用它,这意味着可以安全地释放托管资源。

这个内部方法可以被赋予任意名称,比如"CoreDispose"或"MyInternalDispose",但是惯例上称之为Dispose(Boolean)

protected void Dispose(Boolean disposing)

但是一个更有帮助的参数名称可能是:
protected void Dispose(Boolean itIsSafeToAlsoFreeManagedObjects)
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);

   //Free managed resources too, but only if I'm being called from Dispose
   //(If I'm being called from Finalize then the objects might not exist
   //anymore
   if (itIsSafeToAlsoFreeManagedObjects)  
   {    
      if (this.databaseConnection != null)
      {
         this.databaseConnection.Dispose();
         this.databaseConnection = null;
      }
      if (this.frameBufferImage != null)
      {
         this.frameBufferImage.Dispose();
         this.frameBufferImage = null;
      }
   }
}

然后您可以将 IDisposable.Dispose() 方法的实现更改为:

public void Dispose()
{
   Dispose(true); //I am calling you from Dispose, it's safe
}

并将您的终结器设置为:

~MyObject()
{
   Dispose(false); //I am *not* calling you from Dispose, it's *not* safe
}

注意:如果您的对象是从实现Dispose的对象继承而来的,请在重写Dispose时不要忘记调用它们的基Dispose方法。
public override void Dispose()
{
    try
    {
        Dispose(true); //true: safe to free managed resources
    }
    finally
    {
        base.Dispose();
    }
}

一切都很好,但你可以做得更好


如果用户在您的对象上调用Dispose(),那么一切都已经清理干净了。稍后,当垃圾收集器过来并调用Finalize时,它将再次调用Dispose
这不仅浪费资源,而且如果您的对象对您已经从上一次Dispose()中处理的对象有垃圾引用,那么您将尝试再次处理它们!
您会注意到,在我的代码中,我小心地删除了我已处理的对象的引用,因此我不会尝试在垃圾对象引用上调用Dispose。但这并没有阻止一个微妙的错误悄然而至。
当用户调用Dispose()时:句柄CursorFileBitmapIconServiceHandle被销毁。稍后,当垃圾收集器运行时,它将尝试再次销毁相同的句柄。
protected void Dispose(Boolean iAmBeingCalledFromDisposeAndNotFinalize)
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle); //<--double destroy 
   ...
}

你可以通过告诉垃圾回收器无需对该对象进行终结来解决这个问题——它的资源已经被清理干净,不需要再做更多的工作。在Dispose()方法中调用GC.SuppressFinalize()即可实现此功能。
public void Dispose()
{
   Dispose(true); //I am calling you from Dispose, it's safe
   GC.SuppressFinalize(this); //Hey, GC: don't bother calling finalize later
}

现在用户已经调用了Dispose(),我们有:

  • 释放非托管资源
  • 释放托管资源

没有必要让GC运行终结器-一切都被处理了。

我不能使用Finalize来清理非托管资源吗?

Object.Finalize的文档说:

在对象被销毁之前,Finalize方法用于对当前对象持有的非托管资源执行清理操作。

但MSDN文档还说,对于IDisposable.Dispose

执行与释放、释放或重置非托管资源相关联的应用程序定义任务。

那么哪个是正确的?哪一个是我清理非托管资源的地方?答案是:

由你决定!但选择Dispose

你当然可以将未托管的清理工作放在终结器中:

~MyObject()
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);

   //A C# destructor automatically calls the destructor of its base class.
}

这样做的问题在于你不知道垃圾收集器何时会完成对你的对象的终结。你未受管制、不需要、未使用的本地资源将一直存在,直到垃圾收集器最终运行。然后它将调用你的终结器方法;清理未经管理的资源。 Object.Finalize 的文档指出了这一点:

终结器执行的确切时间是未定义的。为了确保您的类实例的资源能够确定性释放,请实现一个 Close 方法或提供一个 IDisposable.Dispose 实现。

这就是使用 Dispose 清理未受管制资源的优点;你可以知道并控制未受管制资源何时被清理。它们的销毁是“确定性”的。

回答你最初的问题:为什么不现在释放内存,而是等到垃圾回收器决定释放呢?我有一个面部识别软件,需要立即清除掉530 MB的内部图像因为它们不再需要。如果我们不这样做:机器就会陷入交换停顿。

额外阅读

对于喜欢这种解释为什么,以便如何变得明显的人,我建议您阅读唐·博克斯(Don Box)的《Essential COM》第一章:

在35页中,他解释了使用二进制对象的问题,并在你的眼前发明了COM。一旦你意识到COM的为什么,剩下的300页就很明显了,只是详细介绍了Microsoft的实现。

我认为每个曾经处理过对象或COM的程序员,至少应该阅读第一章。这是有史以来最好的解释。

额外奖励阅读

当你所知道的一切都是错的 存档作者:Eric Lippert

因此,编写正确的终结器非常困难,我能给你的最好建议就是不要尝试


45
这是一个很棒的答案,但我认为它会受益于最后附上标准情况下和派生自已经实现Dispose的基类情况下的代码清单。例如,在这里阅读(http://msdn.microsoft.com/en-us/library/aa720161%28v=vs.71%29.aspx)后,我对从已经实现Dispose的类派生时应该如何处理感到困惑(嘿,我刚接触这个)。 - integra753
3
如果你编写的代码正确,就不需要使用最终器/Dispose(bool)方法。我不是在防范自己;我是在防范其他可能不会每次都做对的数以十、百、千甚至百万计开发人员。有时开发人员会忘记调用.Dispose。有时你不能使用 using。我们遵循.NET / WinRT的“成功之坑”方法。我们缴纳开发人员税,编写更好的防护性代码,使其对这些问题具有弹性。 - Ian Boyd
3
但是你并不总是需要为“公众”编写代码。但是,当尝试为一个获得2k+赞的回答制定最佳实践时,这个回答是为了一般介绍非托管内存,最好提供最好的代码示例。我们不想把所有东西都留下来,让人们艰难地摸索。因为这就是现实——每年有成千上万的开发人员学习关于Dispose的微妙之处。没有必要让他们变得更加困难。 - Ian Boyd
2
@toha 这是两种方法。实际上,您可以忽略其中一种,只留下 Dispose - 在其中仅清理非托管资源。您不必重写终结器并尝试在终结期间调用 Dispose - 使用您的类的开发人员必须(强制性的,没有任何条件)自己调用 .Dispose。如果他们没有调用 .Dispose:那么这是他们没有遵循基本规则的问题。其余的答案是为那些想要尝试使他们的类更具有抗击坏程序员能力的开发人员准备的。(即“你可以做得更好”) - Ian Boyd
2
我读过的最好的答案。 @IanBoyd 感谢您宝贵的时间,提供如此富有洞见和资源的答案。 你应该得到这些赞! - Keyrad
显示剩余15条评论

81

IDisposable常常被用来利用using语句,从而方便地进行托管对象的确定性清理。

public class LoggingContext : IDisposable {
    public Finicky(string name) {
        Log.Write("Entering Log Context {0}", name);
        Log.Indent();
    }
    public void Dispose() {
        Log.Outdent();
    }

    public static void Main() {
        Log.Write("Some initial stuff.");
        try {
            using(new LoggingContext()) {
                Log.Write("Some stuff inside the context.");
                throw new Exception();
            }
        } catch {
            Log.Write("Man, that was a heavy exception caught from inside a child logging context!");
        } finally {
            Log.Write("Some final stuff.");
        }
    }
}

51
Dispose模式的目的是提供一种机制来清理托管和非托管资源,并且清理的时间取决于Dispose方法的调用方式。在您的示例中,使用Dispose实际上与清除列表无关,因为清空列表不会影响该集合被处理。同样,将变量设置为null的调用也不会对垃圾回收器产生影响。
您可以查看这篇文章(article)获取有关如何实现Dispose模式的更多细节,但它基本上是这样的:
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)
            {
                // Dispose managed resources.
                if (handle != null)
                {
                    handle.Dispose();
                }
            }

            // Dispose unmanaged managed resources.

            disposed = true;
        }
    }

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

最重要的方法是Dispose(bool),它实际上在两种不同情况下运行:

  • disposing == true:该方法由用户代码直接或间接调用。托管和非托管资源都可以被处理。
  • disposing == false:此方法是由终结器内部的运行时调用的,您不应引用其他对象,只能处理非托管资源。

简单地让垃圾回收器负责清理会存在一个问题,即您无法真正控制垃圾回收器何时运行垃圾回收(虽然可以调用GC.Collect(),但您真的不应该这样做),因此资源可能会比需要的时间更长。请记住,调用Dispose()实际上不会导致垃圾回收器启动回收循环,也不会以任何方式使垃圾回收器收集/释放对象;它只是提供了一种更确定性地清理所使用的资源并告诉垃圾回收器此清理已经完成的方法。

IDisposable和释放模式的整个目的并不是立即释放内存。只有在处理disposing == false场景并操作非托管资源时,Dispose调用才有机会立即释放内存。对于托管代码而言,内存实际上要等到垃圾回收器运行回收循环才会被回收,您真的无法控制(除了调用GC.Collect()之外,我已经提到这样做并不好)。

您的情况并不真实,因为.NET中的字符串不使用任何非托管资源,也不实现IDisposable,因此无法强制它们"清理"。


如果我将 if (handle != null) { handle.Dispose(); } 更改为 if (handle != null) { handle = null; },会有什么区别吗? - toha

24
当对象调用了Dispose方法后,不应再调用其方法(虽然一个对象应该能够容忍多次调用Dispose)。因此,问题中的示例是愚蠢的。如果已经调用了Dispose,则可以丢弃对象本身。因此,用户只需丢弃对整个对象的所有引用(将它们设置为null),与之相关的对象内部将自动清除。
至于关于托管/非托管的一般问题和其他答案中的讨论,我认为任何回答这个问题都必须从未托管资源的定义开始。
问题的关键在于存在一个函数,您可以调用它将系统置于一种状态,并且存在另一个函数,您可以调用它将系统从该状态中恢复出来。现在,在典型示例中,第一个函数可能是返回文件句柄的函数,而第二个函数可能是调用CloseHandle。但是 - 这就是关键 - 它们可以是任何匹配的函数对。一个构建状态,另一个拆除它。如果状态已经构建但尚未拆除,则存在资源实例。您必须安排在正确的时间进行拆除 - 该CLR不管理资源。唯一自动管理的资源类型是内存。有两种类型:GC和堆栈。值类型由堆栈管理(或通过依附于引用类型内部),引用类型由GC管理。
这些函数可能导致可以自由交错的状态更改,也可能需要完美嵌套。状态更改可能是线程安全的,也可能不是。
看看Justice问题中的示例。必须完美嵌套更改日志文件的缩进,否则所有事情都会出错。另外,它们不太可能是线程安全的。
可以利用垃圾收集器搭便车清除非托管资源。但只有当状态更改函数是线程安全的并且两个状态的寿命可以任何方式重叠时才能这样做。因此,Justice资源的示例不得具有终结器!它对任何人都没有帮助。
对于这些类型的资源,您可以只实现IDisposable而没有终结器。终结器绝对是可选的 - 必须是这样。在许多书中,这个问题被掩盖或甚至没有提到。
然后,您必须手动编写Dispose并使其调用您的字段和基类。 C ++ / CLI程序员不必这样做。编译器在大多数情况下为他们编写它。
还有一种替代方案,我更喜欢用于完全嵌套且不是线程安全的状态(除了其他问题之外,避免使用IDisposable使您免于与无法抵抗添加实现IDisposable的每个类的终结器的人争论的问题)。
而不是编写一个类,您可以编写一个函数。该函数接受要回调的委托:
public static void Indented(this Log log, Action action)
{
    log.Indent();
    try
    {
        action();
    }
    finally
    {
        log.Outdent();
    }
}

然后,一个简单的示例会是:

Log.Write("Message at the top");
Log.Indented(() =>
{
    Log.Write("And this is indented");

    Log.Indented(() =>
    {
        Log.Write("This is even more indented");
    });
});
Log.Write("Back at the outermost level again");

传递的 lambda 表达式充当代码块,就像您制作自己的控制结构来完成与 using 相同的目的一样,除了您不再有调用者滥用它的危险性。没有任何方式可以使它们未能清理资源。

如果资源可能具有重叠的生命周期,则此技术的效用较小,因为您希望能够构建资源 A,然后构建资源 B,然后销毁资源 A,然后稍后销毁资源 B。如果强制用户完全嵌套,那么无法执行此操作。但是,您需要使用 IDisposable(但仍然没有终结器,除非您已实现线程安全性,这是需要付出代价的)。


18

我使用IDisposable的情景:清理非托管资源,取消事件订阅,关闭连接。

我用于实现IDisposable的惯用法(不是线程安全的):

class MyClass : IDisposable {
    // ...

    #region IDisposable Members and Helpers
    private bool disposed = false;

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

    private void Dispose(bool disposing) {
        if (!this.disposed) {
            if (disposing) {
                // cleanup code goes here
            }
            disposed = true;
        }
    }

    ~MyClass() {
        Dispose(false);
    }
    #endregion
}

1
这几乎是Microsoft Dispose模式的实现,只是您忘记将Dispose(bool)设置为虚拟的。该模式本身并不是一个很好的模式,除非您绝对需要将dispose作为继承层次结构的一部分,否则应该避免使用它。 - MikeJ

13

是的,那段代码完全是多余且不必要的,而且它并不会让垃圾收集器做任何它原本不会做的事情(一旦MyCollection实例超出范围,也就是出了作用域)。特别是那些.Clear()调用。

关于你的编辑的回答:有点类似。如果我这样做:

public void WasteMemory()
{
    var instance = new MyCollection(); // this one has no Dispose() method
    instance.FillItWithAMillionStrings();
}

// 1 million strings are in memory, but marked for reclamation by the GC

从内存管理的角度来看,它与此函数在功能上完全相同:

public void WasteMemory()
{
    var instance = new MyCollection(); // this one has your Dispose()
    instance.FillItWithAMillionStrings();
    instance.Dispose();
}

// 1 million strings are in memory, but marked for reclamation by the GC

如果你确实非常非常需要立即释放内存,可以调用 GC.Collect()。 但是在此处没有必要这样做。当需要时,内存将被释放。


3
“记忆体会在需要时被释放。” 改为 “当垃圾回收(GC)决定需要时才会被释放。” 在GC决定真正需要内存之前,您可能会遇到系统性能问题。现在释放它可能不是必要的,但可能是有用的。 - Jesse Chisholm
1
有一些特殊情况,将集合中的引用置空可能会加快垃圾回收所涉及的项目。例如,如果创建了一个大数组,并填充了对较小新创建的项目的引用,但在此之后不需要它很长时间,放弃该数组可能会导致这些项目保留到下一个 Level 2 GC,而首先将其清零可能会使这些项目符合下一个 level 0 或 level 1 GC 的条件。当然,将大型短寿命对象放在大对象堆上本来就很麻烦(我不喜欢这种设计),但是…… - supercat
1
在放弃使用这些数组之前将它们清零,可能会减少垃圾回收的影响。 - supercat
在大多数情况下,清空对象并不是必须的,但有些对象可能会保持一堆其他对象的存活状态,即使它们不再需要。将类似于对线程引用的设置为null可能是有益的,但现在可能不再适用了。通常,如果大对象仍然可以在某些方法中被调用以检查它是否已经被清空,那么更复杂的代码并不值得性能提升。请优先考虑代码的清晰度而非“我认为这样会稍微快一点”。 - AyCe

12

如果MyCollection即将被垃圾回收,那么你不需要对其进行处理。这样做只会浪费CPU资源,并且可能会使垃圾回收器已执行的某些预计算分析失效。

我使用IDisposable来确保正确地释放线程和非托管资源等操作。

编辑 针对Scott的评论:

当调用GC.Collect()时,GC性能指标受到影响的唯一时间是

从概念上讲,GC维护对象引用图的视图,以及从线程的堆栈帧中对它的所有引用。此堆可能相当大并跨越许多内存页。为了优化,GC缓存其分析很少更改的页面,以避免不必要地重新扫描页面。当页面中的数据发生更改时,GC会收到来自内核的通知,因此它知道该页面是脏的并需要重新扫描。如果集合在Gen0中,则页面中的其他内容也可能会更改,但在Gen1和Gen2中可能性较小。传闻称,在将GC移植到Mac上以使Silverlight插件在该平台上工作的团队中,这些钩子是不可用的。

另一个反对不必要释放资源的观点:想象一种情况,即进程正在卸载。再想象一下,该进程已运行了一段时间。很可能该进程的许多内存页面已经交换到磁盘上。至少它们已经不在L1或L2缓存中了。在这种情况下,对于正在卸载的应用程序来说,没有必要将所有这些数据和代码页面重新交换到内存中以“释放”操作系统在进程终止时将释放的资源。这适用于托管甚至某些非托管资源。只有保持非后台线程活动的资源必须被处理,否则进程将仍然存在。

在正常执行期间,存在必须正确清理的临时资源(正如@fezmonkey所指出的数据库连接、套接字、窗口句柄),以避免未管理的内存泄漏。这些是必须被处理的内容。如果您创建了一个拥有线程的类(我指的是它创建了该线程,因此负责确保它停止,至少按照我的编码风格),那么该类很可能必须实现IDisposable并在Dispose期间关闭线程。

.NET框架使用IDisposable接口作为信号,甚至警告开发人员,这个类必须被处理。我想不出任何在框架中实现IDisposable(不包括显式接口实现)的类型,其中处理是可选的。


调用Dispose是完全有效、合法和被鼓励的。实现IDisposable接口的对象通常都有其原因。只有在调用GC.Collect()时,才会影响GC性能指标。 - Scott Dorman
对于许多 .net 类,处理是“有点”可选的,这意味着只要不疯狂创建新实例并抛弃它们,通常就不会有任何问题。例如,控件的编译器生成的代码似乎在实例化控件时创建字体,并在窗体被处理时丢弃它们;如果创建和处理成千上万个控件,这可能会占用数千个 GDI 句柄,但在大多数情况下,控件并没有被频繁地创建和销毁。尽管如此,仍应尽量避免这种抛弃行为。 - supercat
1
在字体方面,我怀疑问题在于Microsoft从未真正定义哪个实体负责处理分配给控件的“字体”对象;在某些情况下,控件可能会与一个生命周期更长的对象共享字体,因此让控件Dispose字体将是不好的。在其他情况下,字体将被分配给控件,而没有其他地方,因此如果控件不处理它,就没有人会处理。顺便说一句,如果有一个单独的不可处理的FontTemplate类,这种字体困难本可以避免,因为控件似乎不使用其Font的GDI句柄。 - supercat

8
我不会重复关于使用或释放非托管资源的常规知识,这些都已经被涵盖了。但我想指出一个普遍的误解。
考虑以下代码:
Public Class LargeStuff
  Implements IDisposable
  Private _Large as string()
'一些奇怪的代码,导致_Large现在包含数百万个长字符串。
Public Sub Dispose() Implements IDisposable.Dispose _Large=Nothing End Sub
我知道该Dispose实现不符合当前的准则,但希望你们都能理解。
现在,当调用Dispose时,有多少内存被释放了呢?

答案:没有。
调用Dispose可以释放非托管资源,但它无法回收托管内存,只有GC才能做到。这并不是说上述方法不好,事实上,按照上述模式仍然是一个好主意。一旦运行了Dispose,GC就可以回收_Large使用的内存,即使LargeStuff实例仍在作用域之内。_Large中的字符串可能也在gen0中,但LargeStuff的实例可能在gen2中,因此内存将更快地被回收。
但是,添加一个终结器以调用上述Dispose方法是没有意义的。那只会延迟回收内存,以允许终结器运行。

1
如果LargeStuff实例存在的时间足够长,已经到达第二代,并且_Large持有对新创建的字符串的引用,该字符串位于第零代中,则如果未将LargeStuff实例取消引用并将_Large置空,则由_Large引用的字符串将一直保留到下一个Gen2集合。 将_Large清零可以使字符串在下一个Gen0收集时被消除。 在大多数情况下,清空引用是没有帮助的,但也有一些情况可以提供一些好处。 - supercat

7

7
在你发布的示例中,它仍然没有“立即释放内存”。所有内存都是垃圾回收的,但它可能会允许内存在更早的中被回收。您需要运行一些测试来确保。

框架设计指南是指南,而不是规则。它们告诉您接口的主要用途,何时使用它,如何使用它以及何时不使用它。

我曾经阅读过一个简单的 RollBack() 代码,它利用了 IDisposable。下面的 MiniTx 类会在 Dispose() 上检查标志,如果 Commit 调用从未发生,则会在自身上调用 Rollback。这增加了一层间接性,使调用代码更易于理解和维护。结果看起来像:

using( MiniTx tx = new MiniTx() )
{
    // code that might not work.

    tx.Commit();
} 

我也见过计时/日志代码做同样的事情。在这种情况下,Dispose()方法停止计时器并记录块已退出。
using( LogTimer log = new LogTimer("MyCategory", "Some message") )
{
    // code to time...
}

下面是一些具体的例子,它们没有进行任何未受控资源的清理,但成功地使用了IDisposable来创建更干净的代码。


看看 @Daniel Earwicker 使用高阶函数的例子。对于基准测试、计时、日志记录等,它似乎更加直观。 - Aluan Haddad

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接