ChatGPT解决这个技术问题 Extra ChatGPT

正确使用 IDisposable 接口

我通过阅读 Microsoft documentation 知道 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 包含一百万个字符串,并且您希望现在释放该内存,而不是等待垃圾收集器。上面的代码能做到这一点吗?

我喜欢接受的答案,因为它告诉您使用 IDisposable 的正确“模式”,但就像 OP 在他的编辑中所说的那样,它没有回答他的预期问题。 IDisposable 不会“调用”GC,它只是将对象“标记”为可销毁。但是“现在”释放内存而不是等待 GC 启动的真正方法是什么?我认为这个问题值得更多讨论。
IDisposable 不标记任何内容。 Dispose 方法执行它必须做的事情来清理实例使用的资源。这与GC无关。
@约翰。我明白IDisposable。这就是为什么我说接受的答案没有回答 OP 关于 IDisposable 是否有助于 <i>释放内存</i> 的预期问题(和后续编辑)。由于 IDisposable 与释放内存无关,只有资源,所以就像你说的那样,根本不需要将托管引用设置为 null ,这就是 OP 在他的示例中所做的。所以,他的问题的正确答案是“不,它不能更快地释放内存。事实上,它根本不能帮助释放内存,只有资源”。但无论如何,感谢您的意见。
@desigeek:如果是这种情况,那么您不应该说“IDisposable 不会'调用' GC,它只是将对象'标记'为可销毁”
@desigeek:没有确定性释放内存的保证方法。您可以调用 GC.Collect(),但这是礼貌的请求,而不是要求。必须暂停所有正在运行的线程才能继续进行垃圾收集 - 如果您想了解更多信息,请阅读 .NET 安全点的概念,例如 msdn.microsoft.com/en-us/library/678ysw69(v=vs.110).aspx 。如果线程不能被挂起,例如因为调用了非托管代码,GC.Collect() 可能什么都不做。

4
42 revs, 22 users 80%

Dispose 的目的是释放非托管资源。它需要在某个时候完成,否则它们将永远不会被清理干净。垃圾收集器不知道如何IntPtr 类型的变量调用 DeleteHandle(),它不知道是否需要调用 {1 }。

注意:什么是非托管资源?如果您在 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 的内存就坐在那里 - 等待垃圾收集器最终出现并释放它吗?如果有 open database connection 怎么办?当然,我们不希望该连接处于打开状态,等待 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(),我们仍然可以保存他们的培根!我们仍然可以将其称为 for 他们:当垃圾收集器最终开始释放(即最终确定)我们的对象时。

注意:垃圾收集器最终将释放所有托管对象。当它这样做时,它会调用对象的 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() 代码中,您尝试摆脱的 managed 对象(因为您想提供帮助)完全有可能不再存在:

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() 都调用 third(!) 方法;如果您从 Dispose()(而不是 Finalize())调用它,则传递一个布尔值,这意味着释放托管资源是安全的。

这个internal方法可以被赋予一些任意名称,例如“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

这不仅浪费,而且如果您的对象对您在 last 调用 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

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

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

这是你的选择!但选择处置。

您当然可以将非托管清理放在终结器中:

~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 清理非托管资源的优点;您可以了解并控制何时清理非托管资源。它们的破坏是“确定性的”

回答您最初的问题:为什么不现在释放内存,而不是在 GC 决定时释放内存?我有一个面部识别软件,现在需要删除 530 MB 的内部图像,因为它们不再需要。当我们不这样做时:机器会停止交换。

奖金阅读

对于任何喜欢这个答案风格的人(解释原因,所以如何变得显而易见),我建议你阅读 Don Box 的 Essential COM 的第一章:

直接链接:Pearson Publishing 的第 1 章示例

磁铁:84bf0b960936d677190a2be355858e80ef7542c0

在 35 页中,他解释了使用二进制对象的问题,并在您眼前发明了 COM。一旦你了解了 COM 的原因,剩下的 300 页就很明显了,只是详细介绍了微软的实现。

我认为每个处理过对象或 COM 的程序员至少应该阅读第一章。这是对任何事情的最好解释。

额外的奖金阅读

When everything you know is wrong archive埃里克·利珀特

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


这是一个很好的答案,但我认为它会受益于标准案例的最终代码清单以及类派生自已经实现 Dispose 的基类的情况。例如,我也读过这里(msdn.microsoft.com/en-us/library/aa720161%28v=vs.71%29.aspx),我对从已经实现 Dispose 的类派生时应该做什么感到困惑(嘿,我是新手)。
Dispose() 调用期间将托管实例设置为 null 有什么影响,除了确保它们不会因为 != null 检查失败而再次被释放?不是 Disposable 的托管类型怎么办?是否应该在 Dispose 方法中处理它们(例如设置为 null)?应该对所有托管对象执行此操作,还是仅对那些我们认为“重”且值得在 GC 启动之前进行任何操作的对象执行此操作?我希望它仅适用于类的 Disposable 成员,但作为示例提到的 system.Drawing.Image 似乎不是一次性的......
@Bartosz 您可以在 Dispose 方法中将任何您喜欢的变量设置为 null。将变量设置为 null 意味着它只会可能更快地被收集(因为它没有未完成的引用)。如果一个对象没有实现 IDisposable,那么您不必处置它。只有当对象需要被释放时,它才会公开 Dispose
@Ayce“如果你编写正确的代码,你永远不需要终结器/Dispose(bool) 东西。”我不是在保护我;我正在保护数十、数百、数千或数百万可能每次都无法正确处理的其他开发人员。有时开发人员会忘记调用 .Dispose。有时您不能使用 using。我们遵循“成功之坑” 的.NET/WinRT 方法。我们为开发人员缴纳税款,并编写更好的防御性代码以使其能够应对这些问题。
“但你不必总是为‘公众’写代码。”但是,当试图为 2k+ 投票的答案提出最佳实践时,意味着对非托管内存的一般介绍,最好提供最好的代码示例。我们不想把这一切都抛在脑后——让人们艰难地陷入这一切。因为这就是现实 - 每年都有成千上万的开发人员学习有关 Disposing 的细微差别。没有必要让他们不必要地更难。
y
yfeldblum

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.");
        }
    }
}

S
Scott Dorman

Dispose 模式的目的是提供一种机制来清理托管和非托管资源,何时发生取决于调用 Dispose 方法的方式。在您的示例中,使用 Dispose 实际上并没有执行与 dispose 相关的任何操作,因为清除列表对正在处理的集合没有影响。同样,将变量设置为 null 的调用对 GC 也没有影响。

您可以查看此 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 负责清理的问题在于,您无法真正控制 GC 何时运行收集周期(您可以调用 GC.Collect(),但实际上不应该),因此资源可能会保留大约比需要的时间长。请记住,调用 Dispose() 实际上不会导致收集周期或以任何方式导致 GC 收集/释放对象;它只是提供了一种更确定性地清理所用资源的方法,并告诉 GC 已经执行了此清理。

IDisposable 和 dispose 模式的重点不是立即释放内存。对 Dispose 的调用实际上甚至有机会立即释放内存的唯一一次是在它处理 disposing == false 场景和操作非托管资源时。对于托管代码,内存实际上不会被回收,直到 GC 运行一个收集周期,你真的无法控制(除了调用 GC.Collect(),我已经提到这不是一个好主意)。

您的方案实际上并不有效,因为 .NET 中的字符串不使用任何未管理的资源并且不实现 IDisposable,因此无法强制“清理”它们。


D
Daniel Earwicker

在对对象调用 Dispose 之后,不应再调用对象的方法(尽管对象应该容忍对 Dispose 的进一步调用)。因此,问题中的示例很愚蠢。如果调用 Dispose,则可以丢弃对象本身。因此,用户应该丢弃对整个对象的所有引用(将它们设置为 null),并且它内部的所有相关对象都将自动被清理。

至于关于托管/非托管的一般问题以及其他答案中的讨论,我认为对这个问题的任何答案都必须从非托管资源的定义开始。

归根结底,您可以调用一个函数将系统置于某个状态,还可以调用另一个函数将其从该状态中恢复。现在,在典型示例中,第一个可能是返回文件句柄的函数,第二个可能是对 CloseHandle 的调用。

但是——这是关键——它们可以是任何匹配的函数对。一个建立一个状态,另一个破坏它。如果状态已建立但尚未拆除,则资源实例存在。您必须安排在正确的时间进行拆卸 - 资源不由 CLR 管理。唯一自动管理的资源类型是内存。有两种:GC和堆栈。值类型由堆栈管理(或通过在引用类型中搭便车),引用类型由 GC 管理。

这些函数可能会导致可以自由交错的状态更改,或者可能需要完美嵌套。状态更改可能是线程安全的,也可能不是。

看看Justice的问题中的例子。对日志文件缩进的更改必须完美嵌套,否则一切都会出错。它们也不太可能是线程安全的。

可以搭便车与垃圾收集器一起清理非托管资源。但前提是状态更改函数是线程安全的,并且两个状态的生命周期可以以任何方式重叠。因此,Justice 的资源示例不能有终结器!它只是不会帮助任何人。

对于这些类型的资源,您可以只实现 IDisposable,而无需终结器。终结器是绝对可选的——它必须是。这在许多书中被掩盖甚至没有提及。

然后,您必须使用 using 语句来确保调用 Dispose。这本质上就像在堆栈中搭便车(因此终结器对于 GC,using 对于堆栈)。

缺少的部分是您必须手动编写 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 (但仍然没有终结器,除非你已经实现了线程安全,它不是免费的)。


J
Jake1164

我使用 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
}

这几乎是 Microsoft Dispose 模式的实现,只是您忘记将 DIspose(bool) 设为虚拟。模式本身不是一个很好的模式,应该避免使用,除非您绝对必须将 dispose 作为继承层次结构的一部分。
m
mqp

是的,该代码是完全多余和不必要的,它不会使垃圾收集器做任何它不会做的事情(一旦 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()。不过,这里没有理由这样做。内存将在需要时被释放。


回复:“内存将在需要时被释放。”而是说“当 GC 决定需要它时”。在 GC 确定确实需要内存之前,您可能会看到系统性能问题。现在释放它可能不是必需的,但可能有用。
在某些极端情况下,将集合中的引用归零可能会加快对所引用项目的垃圾收集。例如,如果创建了一个大数组并填充了对较小的新创建项目的引用,但此后很长时间都不需要它,放弃该数组可能会导致这些项目保留到下一次 2 级 GC,而首先将其归零可能会使项目符合下一个 0 级或 1 级 GC 的条件。可以肯定的是,在大对象堆上拥有大的短期对象无论如何都是令人讨厌的(我不喜欢这个设计)但是......
...在放弃它们之前将这些数组清零有时会减轻 GC 的影响。
在大多数情况下,不需要清空东西,但有些对象实际上也可能使一堆其他对象保持活动状态,即使它们不再需要。将诸如对 Thread 的引用设置为 null 可能是有益的,但现在可能不是。如果大对象仍然可以在某种检查它是否已经被清空的方法中被调用,那么更复杂的代码通常不值得性能提升。喜欢干净而不是“我认为这稍微快一些”。
D
Drew Noakes

如果无论如何都要对 MyCollection 进行垃圾收集,那么您不需要将其丢弃。这样做只会过度消耗 CPU,甚至可能使垃圾收集器已经执行的一些预先计算的分析无效。

我使用 IDisposable 来执行诸如确保正确处理线程以及非托管资源之类的事情。

编辑回应斯科特的评论:

影响 GC 性能指标的唯一时间是调用 [sic] GC.Collect() 时”

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

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

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

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


调用 Dispose 是完全有效、合法和鼓励的。实现 IDisposable 的对象通常这样做是有原因的。影响 GC 性能指标的唯一时间是调用 GC.Collect() 时。
对于许多 .net 类来说,处置“在某种程度上”是可选的,这意味着“通常”放弃实例不会造成任何麻烦,只要人们不会疯狂地创建新实例并放弃它们。例如,编译器生成的控件代码似乎会在控件实例化时创建字体,而在处理表单时放弃它们;如果创建和处置数千个控件,这可能会占用数千个 GDI 句柄,但在大多数情况下,控件不会被创建和销毁那么多。尽管如此,人们仍应尽量避免这种放弃。
对于字体,我怀疑问题在于微软从未真正定义过哪个实体负责处理分配给控件的“字体”对象。在某些情况下,控件可能会与寿命较长的对象共享字体,因此让控件 Dispose 字体会很糟糕。在其他情况下,字体将分配给控件而不是其他任何地方,因此如果控件不处理它,没有人会处理它。顺便说一句,如果有一个单独的非一次性 FontTemplate 类,则可以避免字体的这种困难,因为控件似乎不使用其字体的 GDI 句柄。
R
Robert Paulson

在您发布的示例中,它仍然没有“立即释放内存”。所有内存都被垃圾收集,但它可能允许在更早的 generation 中收集内存。您必须进行一些测试才能确定。

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

我曾经读过代码,它是一个简单的 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 使用高阶函数的例子。对于基准测试、计时、日志记录等。它似乎更简单。
p
pipTheGeek

我不会重复有关使用或释放非托管资源的常见内容,这些内容都已涵盖。但我想指出一个常见的误解。给定以下代码

Public Class LargeStuff
  Implements IDisposable
  Private _Large as string()

  'Some strange code that means _Large now contains several million long strings.

  Public Sub Dispose() Implements IDisposable.Dispose
    _Large=Nothing
  End Sub

我意识到 Disposable 实现不遵循当前的指导方针,但希望你们都明白这一点。现在,当调用 Dispose 时,释放了多少内存?答:没有。调用 Dispose 可以释放非托管资源,它不能回收托管内存,只有 GC 可以这样做。这并不是说上面不是一个好主意,实际上遵循上面的模式仍然是一个好主意。运行 Dispose 后,没有什么可以阻止 GC 重新声明 _Large 正在使用的内存,即使 LargeStuff 的实例可能仍在范围内。 _Large 中的字符串也可能在 gen 0 中,但 LargeStuff 的实例可能在 gen 2 中,所以同样,内存会被更快地回收。但是,添加终结器来调用上面显示的 Dispose 方法是没有意义的。这只会延迟内存的重新声明以允许终结器运行。


如果 LargeStuff 的实例已经存在足够长的时间以使其进入第 2 代,并且如果 _Large 持有对第 0 代中新创建的字符串的引用,那么如果 LargeStuff 的实例被放弃如果不清空 _Large,则 _Large 引用的字符串将保留到下一个 Gen2 集合。清零 _Large 可能会使字符串在下一个 Gen0 集合中被消除。在大多数情况下,取消引用没有帮助,但在某些情况下它可以提供一些好处。
f
franckspike

如果要立即删除,请使用非托管内存。

看:

Marshal.AllocHGlobal

元帅.FreeHGlobal

Marshal.DestroyStructure


A
Arjan Einbu

如果有的话,我希望代码的效率低于将其排除在外时的效率。

调用 Clear() 方法是不必要的,如果 Dispose 没有这样做,GC 可能不会这样做......


P
Pragmateek

除了作为控制系统资源生命周期的主要用途(完全被 Ian 的精彩回答所涵盖,赞!)之外,IDisposable/using 组合还可用于确定(关键)全局资源的状态更改范围:控制台、线程、进程、任何全局对象,如应用程序实例。

我写过一篇关于这种模式的文章:http://pragmateek.com/c-scope-your-global-state-changes-with-idisposable-and-the-using-statement/

它说明了如何以可重用和可读的方式保护一些常用的全局状态:控制台颜色、当前线程文化、Excel 应用程序对象属性......


M
MikeJ

我看到很多答案已经转向谈论将 IDisposable 用于托管和非托管资源。我建议这篇文章作为我找到的关于 IDisposable 应该如何实际使用的最佳解释之一。

https://www.codeproject.com/Articles/29534/IDisposable-What-Your-Mother-Never-Told-You-About

对于实际问题;如果您使用 IDisposable 来清理占用大量内存的托管对象,那么简短的回答是否定的。原因是一旦持有内存的对象超出范围,它就可以被收集了。此时,任何引用的子对象也超出范围并将被收集。

唯一真正的例外是,如果您在托管对象中占用了大量内存,并且您已阻止该线程等待某些操作完成。如果在该调用完成后不再需要那些对象,那么将这些引用设置为 null 可能允许垃圾收集器更快地收集它们。但这种情况将代表需要重构的错误代码——而不是 IDisposable 的用例。


我不明白为什么有人在你的回答中加上 -1
我看到的一个问题是人们一直认为使用 using 语句打开文件会使用 Idisposable。当 using 语句完成时,它们不会关闭,因为 GC 将垃圾收集调用处置,yada yada 并且文件将被关闭。相信我,确实如此,但还不够快。有时需要立即重新打开同一个文件。这就是 VS 2019 .Net Core 5.0 目前正在发生的事情
@LawrenceThurman,您似乎在描述人们使用一次性用品而不使用 using 语句,而是在具有终结器的类上。 GC 不调用 dispose 它调用终结器。例如,如果将 FIleStream 包装在 using 语句中,则在处置时将关闭文件。
@MikeJ 试试看 - 我向你保证我知道我在说什么。用 using 语句打开一个文件,修改它关闭并立即尝试重新打开同一个文件并再次修改它。现在连续做 30 次。我曾经每小时处理 750,000 张 jpg 来构建构建 pdf,并将原始彩色 jpg 转换为黑白。 .jpg这些 Jpg 是从账单中扫描出来的页面,有些有 10 页。 GC 会变慢,尤其是当您有一台具有 256 GB 内存的机器时。它在机器需要更多内存时收集,
它只寻找在它看起来时没有被使用的对象。您需要在 using 语句结束之前调用 file.Close() 。哦,是的,也可以尝试使用数据库连接,使用实数,800,000 个连接,您知道大型银行可能会使用,这就是人们使用连接池的原因。
C
CharithJ

您给定的代码示例不是 IDisposable 用法的好示例。字典清除通常不应该使用 Dispose 方法。字典项目超出范围时将被清除和处置。 IDisposable 需要实施以释放一些即使超出范围也不会释放/释放的内存/处理程序。

以下示例显示了 IDisposable 模式的一个很好的示例,其中包含一些代码和注释。

public class DisposeExample
{
    // A base class that implements IDisposable. 
    // By implementing IDisposable, you are announcing that 
    // instances of this type allocate scarce resources. 
    public class MyResource: IDisposable
    {
        // Pointer to an external unmanaged resource. 
        private IntPtr handle;
        // Other managed resource this class uses. 
        private Component component = new Component();
        // Track whether Dispose has been called. 
        private bool disposed = false;

        // The class constructor. 
        public MyResource(IntPtr handle)
        {
            this.handle = handle;
        }

        // Implement IDisposable. 
        // Do not make this method virtual. 
        // A derived class should not be able to override this method. 
        public void Dispose()
        {
            Dispose(true);
            // This object will be cleaned up by the Dispose method. 
            // Therefore, you should call GC.SupressFinalize to 
            // take this object off the finalization queue 
            // and prevent finalization code for this object 
            // from executing a second time.
            GC.SuppressFinalize(this);
        }

        // Dispose(bool disposing) executes in two distinct scenarios. 
        // If disposing equals true, the method has been called directly 
        // or indirectly by a user's code. Managed and unmanaged resources 
        // can be disposed. 
        // If disposing equals false, the method has been called by the 
        // runtime from inside the finalizer and you should not reference 
        // other objects. Only unmanaged resources can be disposed. 
        protected virtual void Dispose(bool disposing)
        {
            // Check to see if Dispose has already been called. 
            if(!this.disposed)
            {
                // If disposing equals true, dispose all managed 
                // and unmanaged resources. 
                if(disposing)
                {
                    // Dispose managed resources.
                    component.Dispose();
                }

                // Call the appropriate methods to clean up 
                // unmanaged resources here. 
                // If disposing is false, 
                // only the following code is executed.
                CloseHandle(handle);
                handle = IntPtr.Zero;

                // Note disposing has been done.
                disposed = true;

            }
        }

        // Use interop to call the method necessary 
        // to clean up the unmanaged resource.
        [System.Runtime.InteropServices.DllImport("Kernel32")]
        private extern static Boolean CloseHandle(IntPtr handle);

        // Use C# destructor syntax for finalization code. 
        // This destructor will run only if the Dispose method 
        // does not get called. 
        // It gives your base class the opportunity to finalize. 
        // Do not provide destructors in types derived from this class.
        ~MyResource()
        {
            // Do not re-create Dispose clean-up code here. 
            // Calling Dispose(false) is optimal in terms of 
            // readability and maintainability.
            Dispose(false);
        }
    }
    public static void Main()
    {
        // Insert code here to create 
        // and use the MyResource object.
    }
}

M
Michael Burr

Dispose() 操作在示例代码中执行的某些操作可能具有由于 MyCollection 对象的正常 GC 而不会发生的效果。

如果 _theList_theDict 引用的对象被其他对象引用,则该 List<>Dictionary<> 对象将不会被收集,而是突然没有内容。如果没有像示例中那样的 Dispose() 操作,这些集合仍将包含它们的内容。

当然,如果是这种情况,我会称其为损坏的设计 - 我只是指出(我认为是迂腐的)Dispose() 操作可能不是完全多余的,这取决于 { 是否还有其他用途2} 或 Dictionary<> 未在片段中显示。


它们是私有字段,所以我认为假设 OP 没有给出对它们的引用是公平的。
1)代码片段只是示例代码,所以我只是指出可能存在容易忽略的副作用; 2)私有字段通常是 getter 属性/方法的目标 - 可能太多(有些人认为 getter/setter 有点反模式)。
s
supercat

大多数关于“非托管资源”的讨论的一个问题是他们并没有真正定义这个术语,但似乎暗示它与非托管代码有关。虽然许多类型的非托管资源确实与非托管代码交互,但以这种方式考虑非托管资源并没有帮助。

相反,人们应该认识到所有托管资源的共同点:它们都需要一个对象要求一些外部“事物”代表它做某事,这会损害一些其他“事物”,并且另一个实体同意这样做,直到另行通知。如果该对象被遗弃并消失得无影无踪,那么没有任何东西可以告诉外部“事物”它不再需要代表不再存在的对象改变其行为;因此,这件东西的用处将永久减少。

因此,非托管资源代表某个外部“事物”同意代表对象改变其行为,如果该对象被放弃并不再存在,这将无用地损害该外部“事物”的有用性。托管资源是一个对象,它是这种协议的受益者,但如果它被放弃,它已经签约接收通知,并且在它被销毁之前将使用这种通知来整理它的事务。


好吧,IMO,非托管对象的定义很明确;任何非 GC 对象。
@Eonil:非托管对象!=非托管资源。事件之类的事情可以完全使用托管对象来实现,但仍然构成非托管资源,因为——至少在短期对象订阅长期对象的事件的情况下——GC 不知道如何清理它们.
b
bluish

IDisposable 适用于取消订阅事件。


s
shA.t

首先是定义。对我来说,非托管资源意味着一些类,它实现了 IDisposable 接口或使用对 dll 的调用创建的东西。 GC 不知道如何处理这些对象。例如,如果类只有值类型,那么我不认为这个类是具有非托管资源的类。对于我的代码,我遵循以下做法:

如果我创建的类使用了一些非托管资源,那么这意味着我还应该实现 IDisposable 接口以清理内存。一旦我完成使用它就清理对象。在我的 dispose 方法中,我遍历类的所有 IDisposable 成员并调用 Dispose。在我的 Dispose 方法中调用 GC.SuppressFinalize(this) 以通知垃圾收集器我的对象已被清理。我这样做是因为调用 GC 是昂贵的操作。作为额外的预防措施,我尝试多次调用 Dispose() 。有时我添加私有成员 _disposed 并检查方法调用是否已清理对象。如果它被清理然后生成 ObjectDisposedException 以下模板演示了我用文字描述的代码示例:

public class SomeClass : IDisposable
    {
        /// <summary>
        /// As usually I don't care was object disposed or not
        /// </summary>
        public void SomeMethod()
        {
            if (_disposed)
                throw new ObjectDisposedException("SomeClass instance been disposed");
        }

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

        private bool _disposed;

        protected virtual void Dispose(bool disposing)
        {
            if (_disposed)
                return;
            if (disposing)//we are in the first call
            {
            }
            _disposed = true;
        }
    }

“对我来说,非托管资源意味着一些类,它实现了 IDisposable 接口或使用对 dll 的调用创建的东西。”所以你是说is IDisposable本身应该被视为非托管资源的任何类型?这似乎不正确。此外,如果实现类型是纯值类型,您似乎建议不需要处理它。这似乎也是错误的。
每个人都自己判断。我不喜欢仅仅为了添加而在我的代码中添加一些东西。这意味着如果我添加 IDisposable,这意味着我已经创建了某种 GC 无法管理的功能,或者我认为它无法正确管理它的生命周期。
c
controlbox

处置托管资源的最合理用例是准备 GC 回收否则永远不会收集的资源。

一个典型的例子是循环引用。

虽然使用避免循环引用的模式是最佳实践,但如果您最终得到(例如)一个引用回其“父”的“子”对象,如果您只是放弃,这可能会停止父的 GC 收集引用并依赖 GC - 另外,如果您实现了终结器,它将永远不会被调用。

解决此问题的唯一方法是通过将子级上的父引用设置为 null 来手动中断循环引用。

在父母和孩子上实现 IDisposable 是最好的方法。在 Parent 上调用 Dispose 时,对所有 Child 调用 Dispose,并在子 Dispose 方法中,将 Parent 引用设置为 null。


在大多数情况下,GC 不是通过识别死对象来工作,而是通过识别活对象来工作。在每个 gc 循环之后,对于每个已注册完成的对象、存储在大对象堆上或作为活动 WeakReference 的目标的每个对象,系统将检查一个标志,该标志指示在最后一个 GC 周期,并将对象添加到需要立即终结的对象队列中,从大对象堆中释放对象,或者使弱引用无效。如果不存在其他引用,则循环引用不会使对象保持活动状态。