4

据我所知,如果您有一个具有 IDisposable 成员 m 的类 A,则 A 应该实现 IDisposable 并且它应该在其中调用 m.Dispose() 是一个公认的规则。

我找不到令人满意的理由。

我理解如果你有非托管资源,你应该提供一个终结器以及 IDisposable 的规则,这样如果用户没有显式调用 Dispose,终结器仍然会在 GC 期间清理。

但是,有了该规则,您似乎不需要拥有该问题所涉及的规则。例如...

如果我有一堂课:

class MyImage{
  private Image _img;
  ... }

公约规定我应该有MyImage : IDisposable. 但是如果 Image 遵循约定,实现了一个 finalizer,而我不关心资源的及时释放,那还有什么意义呢?

更新

找到了关于我想在这里得到什么的很好的讨论。

4

7 回答 7

22

但是如果 Image 遵循约定,实现了一个 finalizer,而我不关心资源的及时释放,那还有什么意义呢?

您完全错过了 Dispose 的要点。这不是为了您的方便。这是关于可能想要使用这些非托管资源的其他组件的便利性。除非你能保证系统中没有其他代码关心资源的及时释放,用户也不关心资源的及时释放,否则你应该尽快释放你的资源。这是礼貌的做法。

在经典的囚徒困境中,合作者世界中的一个孤独的叛逃者获得了巨大的利益。但在你的情况下,作为一个孤独的背叛者,你通过编写低质量、忽略最佳实践的代码个人节省了几分钟的时间只会带来很小的好处。受苦的是您的用户和他们使用的所有程序,而您几乎一无所获。您的代码利用了其他程序解锁文件并释放互斥锁和所有这些东西的事实。做一个好公民,为他们做同样的事。做起来并不难,它让整个软件生态系统变得更好。

更新:这是我的团队现在正在处理的真实情况的示例。

我们有一个测试实用程序。它有一个“句柄泄漏”,因为一堆非托管资源没有被积极处置;每个“任务”可能会泄漏六个句柄。当它发现禁用的测试等时,它会维护一个“要执行的任务”列表,等等。我们在这个列表中有一万或两万个任务,所以我们很快就得到了这么多未完成的句柄——这些句柄应该死掉并释放回操作系统——很快系统中没有一个不是相关测试可以运行。测试代码不在乎。它工作得很好。但最终被测试的代码无法制作消息框或其他 UI,整个系统要么挂起要么崩溃。

垃圾收集器没有理由知道它需要更积极地运行终结器以更快地释放这些句柄;为什么要呢?它的工作是管理内存。您的工作是管理句柄,因此您必须完成这项工作。

于 2011-05-23T19:35:05.580 回答
6

但是如果 Image 遵循约定,实现了一个 finalizer,而我不关心资源的及时释放,那还有什么意义呢?

那么就没有了,如果你不关心及时发布,你可以确保一次性对象写得正确(事实上我从来没有做过这样的假设,即使是 MS 代码。你永远不知道什么时候不小心滑过)。关键是你应该关心,因为你永远不知道它什么时候会导致问题。考虑一个开放的数据库连接。让它闲置,意味着它不会在池中被替换。如果您有多个请求进入一个,您可能会用完。

如果你不在乎,没有什么说你必须这样做。这样想,就像在非托管程序中释放变量一样。您不必这样做,但这是非常可取的。如果没有其他原因,从程序继承的人不必想知道为什么它没有得到照顾,然后尝试清理它。

于 2011-05-23T18:52:02.240 回答
2

首先,无法保证终结器线程何时清理对象 - 考虑类具有对 sql 连接的引用的情况。除非您确保及时处理,否则您将打开一个未知时间段的连接 - 您将无法重复使用它。

其次,终结不是一个廉价的过程——你应该确保如果你的对象被正确处理,你调用 GC.SuppressFinalize(this) 来防止终结的发生。

扩展“不便宜”方面,终结器线程是一个高优先级线程。如果你给它做太多事情,它会从你的主应用程序中夺走资源。

编辑:好的,这是 Chris Brummie 的一篇关于 Finalization 的博客文章包括为什么它很贵。(我知道我在某处读过很多关于这个的文章)

于 2011-05-23T18:57:55.360 回答
1

如果你不关心资源的及时释放,那确实没有意义。如果您可以确定代码仅供您使用,并且您有大量可用内存/资源,为什么不让 GC 在它选择时将其吸走。OTOH,如果其他人正在使用您的代码并创建 (eg)MyImage的许多实例,那么控制内存/资源使用将非常困难,除非它处理得很好。

于 2011-05-23T18:53:56.280 回答
1

如果您正在使用的对象实现了 IDisposable,它会告诉您当您完成它时它有一些重要的事情要做。重要的事情可能是释放非托管资源,或者从事件中解开挂钩,以便在您认为完成之后它不会处理事件等等。通过不调用 Dispose,您是在说您知道得更好关于该对象的操作方式比原作者。在一些微小的边缘情况下,如果您自己编写了 IDisposable 类,或者您知道与调用 Dispose 相关的错误或性能问题,这实际上可能是正确的。一般来说,忽略一个要求你在完成后处理它的类不太可能是一个好主意。

谈论终结器 - 正如已经指出的那样,它们有一个成本,可以通过处置对象来避免(如果它使用 SuppressFinalize)。不仅仅是运行终结器本身的成本,也不仅仅是在 GC 收集对象之前必须等到终结器完成的成本。带有终结器的对象在被标识为未使用且需要终结器的集合中幸存下来。所以它将被提升(如果它还没有在第 2 代中)。这有几个连锁反应:

  • 下一个更高一代的收集频率会降低,因此在终结器运行后,您可能要等待很长时间,然后 GC 才会到达该代并将您的对象扫走。所以释放内存可能需要更长的时间。
  • 这给对象提升到的集合增加了不必要的压力。如果它从 0 代提升到 1 代,那么现在 1 代将比它需要的更早填满。
  • 这可能导致更高代的垃圾收集更频繁,这是另一个性能损失。
  • 如果在 GC 到达更高代时对象的终结器尚未完成,则可以再次提升该对象。因此,在不好的情况下,您可能会在没有充分理由的情况下将对象从第 0 代提升到第 2 代。

显然,如果您只对一个对象执行此操作,则不太可能花费您任何明显的费用。如果您因为发现对正在使用的对象调用 Dispose 感到厌烦而将其作为常规做法,那么它可能会导致上述所有问题。

Dispose 就像前门上的锁。它的存在可能是有原因的,如果你要离开大楼,你可能应该锁上门。如果锁定它不是一个好主意,就不会有锁。

于 2011-05-26T20:25:48.033 回答
1

许多类要求调用 Dispose 以确保正确性。例如,如果某些 C# 代码使用带有“finally”块的迭代器,则如果使用该迭代器创建枚举器并且未释放该枚举器,则该块中的代码将不会运行。虽然在少数情况下确保在没有终结器的情况下清理对象是不切实际的,但对于大多数依赖终结器来进行正确操作或避免内存泄漏的代码是错误的代码。

如果您的代码获得了 IDisposable 对象的所有权,那么除非对象的 cleass 被密封或您的代码通过调用构造函数(而不是工厂方法)创建对象,否则您无法知道对象的真实类型是什么,以及是否可以安全地放弃。Microsoft 最初可能打算放弃任何类型的对象应该是安全的,但这是不现实的,并且认为放弃任何类型的对象应该是安全的信念是没有帮助的。如果一个对象订阅了事件,允许安全放弃将需要为所有事件添加一个弱间接级别,或者为所有其他访问添加一个(非弱)间接级别。在许多情况下,最好要求调用者正确处理对象,而不是增加大量开销和复杂性以允许放弃。

另请注意,顺便说一句,即使对象试图适应遗弃,它仍然可能非常昂贵。创建一个 Microsoft.VisualBasic.Collection(或其他任何名称),添加一些对象,然后创建和处置一百万个枚举器。没问题——执行非常快。现在创建并放弃一百万个枚举器。除非你每隔几千个枚举器就强制执行一次 GC,否则会打盹。Collection 对象被编写为允许放弃,但这并不意味着它没有重大成本。

于 2011-05-24T15:30:04.190 回答
0

即使您在这种特殊情况下不关心,您仍然应该遵循标准,因为在某些情况下您会关心。制定一个标准并始终根据特定的指导方针遵循它比制定一个您有时无视的标准要容易得多。随着您的团队成长和产品老化,尤其如此。

于 2011-05-23T19:01:58.290 回答