如何说服我的同事不要在所有地方都实现IDisposable?

18

我在一个项目中工作,其中有很多对象由少数几个类实例化,并在应用程序的生命周期内保持在内存中。经常会出现OutOfMemoryExceptions引发的许多内存泄漏。似乎在实例化的对象超出范围后,它们没有被垃圾回收。

我已经确定问题主要是与附加到长期存在的对象上的事件处理程序有关,这些处理程序从未分离,从而导致长期存在的对象仍然引用超出范围的对象,这些对象将永远不会被垃圾回收。

我的同事提出的解决方案是:全面在所有类上实现IDisposable接口,在Dispose方法中将对象中的所有引用设置为null,并分离您附加的所有事件。

我认为这是一个非常糟糕的想法。首先,因为这是“过度设计”,大部分问题可以通过修复一些问题区域来解决;其次,因为IDisposable的目的是释放对象控制的任何非托管资源,而不是因为您不信任垃圾回收器。到目前为止,我的观点听起来都没有人理睬。我该如何说服他们这是徒劳无功的呢?


8
祝你好运,我的朋友。聋耳往往有保持聋的倾向。 - Jason
1
你列举的同事们采取的这些方法,只是对他们不完全理解的问题使用了一个大锤。 - RichardOD
7
IDisposable 的目的是释放需要释放的任何内容,无论是托管资源还是非托管资源。它主要用于释放非托管资源,但我认为完全可以在 IDisposable 中释放依赖项(例如删除事件处理程序)。然而,并不意味着所有类型都必须实现 IDisposable。 - Jon Skeet
我投票关闭此问题,因为它不属于此处讨论范围,而应该在workplace.stackexchange.com上进行讨论,但由于时间过长无法迁移。 - Joshua
11个回答

16

恰巧我刚在别处发布了此评论:

  

对对象的引用被错误保留仍然是资源泄漏。这就是为什么GC程序仍然可能存在泄漏,通常是由于观察者模式 - 观察者在列表中而不是可观察对象上,并且永远不会被移除。最终,每个添加都需要一个remove,就像每个new都需要一个delete。完全相同的编程错误导致完全相同的问题。 "资源"实际上只是一对必须使用相应参数调用相等次数的函数,而"资源泄漏"就是当您未能这样做时发生的情况。

你说:

  

IDisposable的目的是释放对象控制的任何非托管资源

现在,事件上的+-运算符有效地是一对您必须使用相应参数(事件/处理程序对是相应参数)相等次数调用的函数。

因此,它们构成一种资源。由于它们没有被GC处理或“管理”,因此将它们视为另一种未经管理的资源可能很有帮助。正如Jon Skeet在评论中指出的那样,未经管理通常具有特定的含义,但在IDisposable的上下文中,我认为将其扩展以包括任何必须在“建立”后“拆除”的资源非常有帮助。

因此,事件解绑是使用IDisposable进行处理的非常好的选择。

当然,您需要在某个地方调用Dispose,并且不需要在每个对象上实现它(仅需要管理事件关系的对象)。

请注意,如果一对对象通过事件连接,并且您在所有其他对象中失去了它们的所有引用,则它们之间不会使彼此保持存活状态。垃圾回收器(GC)不使用引用计数。一旦对象(或对象组)变得不可访问,它就可以被回收。

您只需要担心那些被列为长时间存在的对象的事件处理程序,例如静态事件AppDomain.UnhandledException,或者是您应用程序的主窗口上的事件。


3
“未经处理的资源”具有一个非常具体的含义,事件无法涵盖这一含义。在这里,事件并没有什么特别之处——只是一个对象对另一个对象拥有不需要的引用。完全没有涉及到未经处理的内容。” - Jon Skeet
1
我同意有关实现IDisposable是完全合理的观点,也同意不是每种类型都应该实现IDisposable。但是我强烈反对你使用"非托管资源"这个术语。 - Jon Skeet
1
我猜这取决于你对“未托管”一词的理解,是指“未编译为IL的内容”还是指“GC无法处理的内容”。为了解释IDisposable的全部价值,我们可以说它适用于“未托管资源以及以托管代码编写但与未托管资源类似的内容”,或者我们可以将“未托管资源”扩大为指GC无法清理的资源。作为库的用户,我不应该关心它是用哪种语言实现的,而应该关心如何正确使用它,它是否线程安全等。这就是为什么我更喜欢更广泛的定义。 - Daniel Earwicker
在这里进行了编辑,以澄清“未托管”一词的范围,参考了Jon的评论。 - Daniel Earwicker
1
你的观点似乎是大多数情况下finalization和dispose是一起使用的。不幸的是,像https://dev59.com/HnM_5IYBdhLWcg3ww2AQ#1243597这样的完整讨论给人的印象就是它们只是为了完整而进入finalizers。实际上,finalizers是一个小众的东西,现在几乎没有理由编写它们。`IDisposable`经常被编写而与finalizers无关。我建议你看看它在C++/CLI中的用法。或者... - Daniel Earwicker
显示剩余10条评论

11

将它们指向Joe Duffy关于IDisposable/finalizers的帖子——多位聪明人的集体智慧。

目前我很难看到那里有说“不需要时不要实现它”的声明,但除此之外,向他们展示正确实现的复杂性可能会有助于阻止他们这样做…

不幸的是,如果人们不听,他们就是不听。试着让他们解释为什么他们认为需要 IDisposable 。他们认为垃圾回收器不起作用吗?告诉他们它是如何工作的。如果你能让他们相信它对大多数类型没有好处,那么他们肯定会停止为自己增加工作量…

正如Brian所说,仅仅实现IDisposable不能单独解决事件问题 - 它需要被调用。在这种情况下,finalizers也无法帮助你。他们确实需要显式地执行某些操作来删除事件处理程序。


9
仅仅在所有类型上实现Dispose()是不能解决问题的。请记住Dispose()不会自动调用,也与回收托管内存无关。为了让Dispose()方法生效,您需要在所有相关的地方显式或通过using调用它。
换句话说,仅仅实现IDisposable并不能神奇地解决您的问题,因为除非您还改变代码中每种类型的使用方式,否则Dispose()方法将不会被调用。
然而,我不建议在所有类型上实现IDisposable,因为这毫无意义。该接口用于指示涉及某些资源的类型,这些资源不受垃圾回收器处理。
事件引用由垃圾回收器处理。如果您的发布者的生命周期明显长于您的订阅者,则只需取消订阅即可。一旦发布者死亡,订阅者也将死亡。

2
这绝对是一个比仅仅确保按需分离订阅者要大得多的任务。 - Brian Rasmussen
@Brian - 即使使用 using 关键字也是这样吗? - Daniel Earwicker
@Earwicker:抱歉,我不确定你在问什么。请详细说明,以便我必要时可以澄清我的答案。 - Brian Rasmussen
@Earwicker - using关键字很容易理解。在所有类中实现IDispose才是伟大的任务。 - michael_erasmus
@Peter Rotham - 嘿,跟我说说吧。实际上,真正困难的部分是说服微软认为实现 IDisposable 值得语言支持。梦想:http://smellegantcode.wordpress.com/2007/11/30/a-new-use-for-the-c-using-keyword/ - 现实:https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=294208 - Daniel Earwicker
显示剩余2条评论

5
我曾经帮助一个同事解决类似的OutOfMemoryException错误问题(由于事件留下对象引用导致)。我首先运行了FXCop来检查代码,它指出没有在一个IDisposable类上调用Dispose方法。将该代码修改为使用Dispose方法解决了这个问题。也许你应该建议使用FXCop?或者在源代码存储库中找到存在问题的代码,在其上运行FXCop-看看它是否突出显示了问题(如果是由.NET Framework类引起的话,它可能会)并用它来说服你的同事。

2

这可不是一件容易的事情,但我发现让别人觉得某件事情是他们自己的想法是实现你想要的目标的最佳方法。虽然你更了解他们,但是像“如果只有一种方法可以找出为什么对象在程序执行后仍然存在”和“我希望能够获得更多关于事件中对象保留的信息”的短语可能是一个好的开始。


这真是有趣。不知为何,它让我想起了《红色警戒》中的“他思维薄弱”的说法。 - RichardOD

2
问问他们是否希望在使用自行车后被强制关闭电机,即使没有电机。或者是否希望在离开工作场所之前,被迫按下椅子、桌子、咖啡杯等物品上的“关闭”按钮,即使没有需要关闭的东西。
实现IDisposable可以强制用户在不再使用对象时显式地告知该对象。如果此对象不需要清理任何内容,则只是一种不必要的复杂性。
顺便说一句,我认为通过实现IDisposable来注销事件是一种适当的清理事件的方式。有东西需要“关闭”。

1

IDisposable仅用于释放非托管资源(如SafeHandles等),并且Dispose方法会在类层次结构中传播。它不是用来试图规避不良的非托管编程实践。


1
我不认为我会走得那么远。它适用于任何需要有序清理的东西。例如,如果您有一个对象池,您可能希望实现IDisposable,以便底层对象可以释放回池中...这并不意味着涉及任何非托管资源。 - Jon Skeet
一个事件是一个未经管理的资源。 - Daniel Earwicker
@supercat:不,依我看,那与传统意义上的非托管资源完全不同。 - Jon Skeet
@Jon Skeet:我想我的观点是,拥有一个术语来描述IDisposable应该清理的内容是有用的。考虑到IDisposable的常引用定义,“非托管资源”这个词组似乎很贴切。我能看出来,finalizer无法与直接钩入事件计划一起使用,使得直接钩入事件与其他“非托管资源”有所不同;但假设事件钩子钩入了一个对象,该对象持有对主对象的弱引用,那么在这种情况下,主对象可能被收集,但它的finalizer必须提供取消订阅... - supercat
这将会有些复杂,因为没有任何要求事件提供一个可以在终结器中安全运行的取消订阅方法。如果主对象的事件被连接起来,那么主对象将成为托管资源(其他代码应该调用主对象的“dispose”,但它的终结器将处理它不调用的情况),而事件订阅将是一个未经管理的资源,它被包装在其中。 - supercat
显示剩余4条评论

1
创建一个职责,解决另一个OutOfMemoryException。当有人解决属于其他人的错误时,会增加对自己代码的责任感。
在我们项目的早期阶段,我们被建议使用“傻瓜旗帜” - 对致命错误负责的人会在一天内获得这个旗帜。

1

提出一个比他们的解决方案更优秀的解决方案 ;-)

例如,一个简单的替代方案是在长期存在的对象中使用WeakReference来持有事件处理程序的引用。这将要求事件处理程序在需要时在其他地方被引用,但是一旦它们超出范围,它们就会被垃圾回收,弱引用可以从长期存在的对象中删除。


是的,但是由于引用已经内置到委托中,你如何在事件处理程序中使用弱引用呢? - eduesing
http://blogs.msdn.com/greg_schechter/archive/2004/05/27/143605.aspx - dtb

0
如果一个对象需要确保在被放弃之前,它的外部可能会比它更长寿的某些东西得到清理,那么该对象需要实现IDisposable。有些对象的属性与其他对象“连接”,因此更改这些属性将更改其他对象;有时需要将这些属性设置为null。在vb.net中,“WithEvents”字段实际上是一个附加和分离事件处理程序的属性,因此vb.net中的WithEvents字段应设置为Nothing。请注意,通常情况下,一个对象实现IDisposable纯粹是为了将其自己的字段置空是没有用处的。虽然有一些情况可能会有所帮助(例如,如果一个已经存在很长时间的对象持有对最近创建的对象的引用,则清除引用可能会使更近期创建的对象比它本来应该更早地被收集),但这并不是必要的。
必要的是确保需要清理其他对象的对象实现IDisposable并确保这些其他对象得到清理。我对Microsoft鼓励人们编写放弃事件处理程序的代码感到恼火。虽然在事件发布者不超过订阅者的情况下可以放弃事件处理程序,而这种情况通常发生在相互链接的GUI元素中,但我真的看不出为什么事件不应该总是作为一项常规清理。

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