IDisposable实现 - 'if(disposing)'应该包含什么

时间:2011-10-04 02:51:13

标签: c# .net .net-4.0 memory-leaks idisposable

我一直在修复winforms应用程序中的一些内存泄漏问题,并注意到一些非显式Disposed的一次性对象(开发人员没有调用Dispose方法)。 Finalize方法的实现也没有帮助,因为它没有进入if (disposing)子句。所有静态事件取消注册和集合清除都已放入if (disposing)子句中。如果对象是一次性的,最好的做法就是调用Dispose,但不幸的是,有时会发生这种情况

如果存在非托管对象,静态事件处理程序和一些托管时需要清除的托管集合。什么是决定应该进入什么以及应该从if (disposing)条款中找出什么的方法。

Dispose method.

// 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)
{
    if (!disposed)
    {
        if (disposing)
        {
            // Free other state (managed objects).
        }

         // Free your own state (unmanaged objects).
         // Set large fields to null.
         disposed = true;
     }
 }

It says托管对象应该在if (disposing)中,只有在开发人员显式调用Dispose方法时才能正常执行。如果已经实现了Finalize方法并且开发人员忘记调用Dispose方法,那么通过Finalizer执行的执行不会进入if (disposing)部分。

以下是我的问题。

  1. 如果我有静态事件处理程序导致内存泄漏,我应该在哪里取消注册?进出if (disposing)条款?

  2. 如果我有一些导致内存泄漏的集合,我应该在哪里清除它们?进出if (disposing)条款?

  3. 如果我使用第三方一次性对象(例如:devExpress winform控件),我不确定它们是托管对象还是非托管对象。假设我想在处理表单时处理它们。我如何知道托管什么以及什么是非托管对象?一次性不说吗?在这种情况下,如何决定应该进入什么以及if (disposing)条款应该包含哪些内容?

  4. 如果我不确定管理或解除管理的事情,那么从if (disposing)条款中处理/清除/取消注册事件可能会产生什么不良后果?假设它在处置之前检查为空?

  5. 修改

    我的意思是因为取消注册的事件如下所示。 Publisher是一个长期存在的实例,下面的行位于订阅者的构造函数中。在这种情况下,订户需要取消注册事件并在发布者之前进行处置。

    publisher.DoSomeEvent += subscriber.DoSomething;
    

6 个答案:

答案 0 :(得分:3)

这里要记住的关键是IDisposable的目的。它的工作是帮助您确定性地释放代码所持有的资源,对象被垃圾收集之前。这实际上是C#语言团队选择关键字using的原因,因为括号确定了应用程序所需的对象及其资源的范围。

例如,如果打开与数据库的连接,则需要释放该连接并在完成连接后尽快关闭它,而不是等待下一次垃圾回收。这就是您实施Disposer的地点和原因。

第二种方案是协助非托管代码。实际上这与操作系统的C ++ / C API调用有关,在这种情况下,您有责任确保代码不会泄露。尽管.Net被编写为简单的P / Invoke到现有的Win32 API,但这种情况很常见。封装来自操作系统的资源(例如Mutex)的任何对象都将实现Disposer,以允许您安全且确定地释放它的资源。

但是,这些API 实现析构函数,以保证如果您没有正确使用资源,它将不会被操作系统泄露。当你的终结器被调用时,你不知道你引用的其他对象是否已被垃圾收集,这就是为什么对它们进行函数调用是不安全的(因为它们可以抛出NullReferenceException),只是非定期引用(根据定义,不能进行垃圾回收)将可用于终结器。

希望有所帮助。

答案 1 :(得分:2)

从广义上讲,托管资源位于if (disposing)内部,非托管资源位于其中。处置模式如下:

  1. if (disposed) {

    如果此物品已经处理掉,请勿再次丢弃。

  2. if (disposing) {

    如果以编程方式请求处置(true),则处置此对象拥有的托管资源(IDisposable对象)。

    如果处理是由垃圾收集器(false)引起的,请不要丢弃托管资源,因为垃圾收集器可能已经处理了所拥有的托管资源,并且会在应用程序终止之前对其进行定义处理。

  3. }

    处置非托管资源并释放对它们的所有引用。第1步确保只发生一次。

  4. disposed = true

    将此物体标记为防止重复处理。重复处理可能会在步骤2或3处导致NullReferenceException。

  5. 问题1
    根本不要在Dispose方法中处理它们。如果您处理了该类的多个实例会发生什么?你每次都要处理静态成员,尽管它们已被处理掉了。我找到的解决方案是处理AppDomain.DomainUnloaded事件并在那里执行静态处理。

    问题2
    这一切都取决于集合的项目是管理还是非管理。可能值得创建托管包装器,为您正在使用的任何非托管类实现IDisposable,确保管理所有对象。

    问题3
    IDisposable是一个托管接口。如果一个类实现了IDisposable,那么它就是一个托管类。在if (disposing)内部署托管对象。如果它没有实现IDisposable,则它是受管理的,不需要处理,或者不受管理,应该在if (disposing)之外部署。

    问题4
    如果应用程序意外终止,或者不使用手动处理,则垃圾收集器会以随机顺序处理所有对象。子对象可以在其父母被处置之前被处置,从而导致孩子被父母第二次处置。大多数托管对象可以安全地多次处理,但前提是它们已经正确构建。如果对象被多次丢弃,你冒险(但不太可能)导致gargabe集合失败。

答案 2 :(得分:0)

如果我有静态事件处理程序导致内存泄漏,我应该在哪里取消注册?进出if(处理)条款?

在实例上调用Dispose方法,其中在类级别使用静态事件处理程序。所以你不应该在处理中取消注册。通常,静态事件处理程序应该在类卸载时取消注册,或者在应用程序执行期间的某个时刻导致不再需要此事件处理程序。

对于所有管理和未管理的资源,更好地实施IDisposable模式。 看这里 http://msdn.microsoft.com/en-us/library/fs2xkftw%28VS.80%29.aspxFinalize/Dispose pattern in C#

答案 3 :(得分:0)

听起来你主要有托管对象,即实现IDisposable的对象。非托管代码可能是IntPtr或句柄,通常意味着调用非托管代码或P / Invoke来访问它们。

  1. 正如马赫普指出的那样,Dispose不是为了这个。当一个对象完成接收事件时,它应该注销它自己。如果那是不可能的,请考虑使用WeakReferences。

  2. 除非这些集合包含需要处理的对象,否则这可能不应该进行处置。如果它们是一次性物品,那么它应该进入“if disposing”区块,你应该对集合中的每个项目进行处理。

  3. 如果它实现了IDisposable,那就是托管

  4. 在终结器调用时,不应访问其他托管代码对象,这是“if(disposing)”块之外的意思。

答案 4 :(得分:0)

除非类的唯一目的是封装某些资源(*),如果放弃需要清理它,它不应该有一个终结器,并且永远不应该调用Dispose(bool),值为False,但是调用Dispose(False)应该没有效果。如果一个继承的类需要保存需要清理的资源(如果放弃),它应该将该资源封装到专门用于该目的的对象中。这样,如果主对象被放弃,并且没有其他人拥有对封装资源的对象的任何引用,那么该对象可以执行其清理,而无需使主对象保持活动以进行额外的GC循环。

顺便说一句,我不喜欢微软处理Disposed标志。我建议非虚拟Dispose方法应该使用Interlocked.Exchange的整数标志,以确保从多个线程调用Dispose只会导致一次执行dispose逻辑。标志本身可能是私有的,但应该有一个受保护和/或公共的Disposed属性,以避免要求每个派生类实现自己的处理标志。

(*)资源不是某种特定类型的实体,而是一个松散的术语,包含一个类可能要求某个外部实体代表它做的任何事情,需要告诉外部实体停止这样做。最典型的情况是,外部实体将授予该类独占使用的某些东西(无论是内存区域,锁定,GDI句柄,文件,套接字,USB设备等),但在某些情况下外部实体可能已经要求实体肯定地做某事(例如,每次发生事件时运行事件处理程序)或保留某些东西(例如,线程静态对象引用)。 “非托管”资源是指如果放弃将无法清理的资源。

顺便说一句,请注意,尽管微软可能已经打算将封装非托管资源的对象清理干净,但有些类型的资源实际上并不实用。例如,考虑一个在线程静态字段中存储对象引用的对象,并在Dispose'd时将该对象引用置空(当然,必须在创建对象的线程上进行处理)。如果对象被放弃但线程仍然存在(例如在线程池中),则线程静态引用的目标可以很容易地无限期地保持活动。即使没有对被放弃对象的任何引用,因此它的Finalize()方法运行,被放弃的对象也很难找到并销毁某个线程中的线程静态引用。

答案 5 :(得分:0)

  

'if(disposing)'

应该包含哪些内容

所有托管对象应该进入if(disposing)子句。托管对象不应该在它的旁边(将通过最终化执行)。

原因是如果该类具有析构函数,则垃圾收集器完成过程可以执行Dispose(false)。通常只有在存在非托管资源时才会有析构函数。垃圾收集器的终结没有特定的顺序来执行Finalize方法。因此,在最终确定发生时,其他托管对象可能不在内存中。