为什么总是需要在具有IDisposable成员的对象上实现IDisposable?

据我所知,这是一个公认的规则,如果你有一个类A具有IDisposable成员m,A应该实现IDisposable,它应该调用它内部的m.Dispose()。

我找不到令人满意的理由,为什么会这样。

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

但是,根据该规则,您似乎不需要具有此问题的规则。 例如…

如果我有课:

class MyImage{ private Image _img; ... } 

约定规定我应该有MyImage : IDisposable 。 但是如果Image遵循惯例并实现了终结器而我不关心资源的及时发布,那有什么意义呢?

UPDATE

我在这里找到了一个很好的讨论。

但是如果Image遵循惯例并实现了终结器而我不关心资源的及时发布,那有什么意义呢?

然后没有一个,如果你不关心及时释放,你可以确保一次性对象被正确写入(事实上我从来没有做过这样的假设,甚至没有使用MS代码。你永远都不知道什么时候意外地滑倒)。 关键是你应该关心,因为你永远不知道什么时候会引起问题。 考虑一个开放的数据库连接。 让它闲置,意味着它不会在池中更换。 如果您有多个请求进入,则可能会用完。

如果你不在乎,没有什么说你必须这样做。 可以这样想,就像在非托管程序中释放变量一样。 你没必要,但这是非常明智的。 如果没有其他原因,inheritance该计划的人不必怀疑为什么没有得到照顾,然后尝试清除它。

但是如果Image遵循惯例并实现了终结器而我不关心资源的及时发布,那有什么意义呢?

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

在经典的囚徒困境中 ,合作社世界中唯一的叛逃者获益匪浅。 但在你的情况下,作为一个孤独的叛逃者只能通过编写低质量,最佳实践忽略的代码,只能为你节省几分钟。 这是你的用户和他们使用的所有程序,你几乎什么也得不到。 您的代码利用了其他程序解锁文件和释放互斥锁以及所有内容的事实。 做一个好公民,为他们做同样的事情。 这并不难,它使整个软件生态系统变得更好。

更新:这是我的团队正在处理的现实情况的一个例子。

我们有一个测试工具。 它有一个“句柄泄漏”,因为一堆非托管资源没有被积极处理; 每个“任务”泄漏可能有六个手柄。 它在发现禁用的测试时会维护一个“要执行的任务”列表,依此类推。 我们在这个列表中有十到两万个任务,所以我们很快就会得到这么多未完成的句柄 – 句柄应该已经死了并释放回操作系统 – 很快系统中没有任何代码都没有与测试相关可以运行。 测试代码不关心。 它工作得很好。 但最终被测试的代码无法制作消息框或其他UI,整个系统也会挂起或崩溃。

垃圾收集器没有理由知道它需要更积极地运行终结器以更快地释放这些句柄; 为什么要这样? 它的工作是管理记忆。 你的工作是管理句柄,所以你必须做那个工作。

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

其次,最终确定并不是一个便宜的过程 – 你应该确保如果你的对象被正确处理掉,你就会调用GC.SuppressFinalize(this)来阻止最终化。

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

编辑:好的,这是Chris Brummie关于Finalization的博客文章,包括为什么它很昂贵。 (我知道我在某处阅读过有关此内容的内容)

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

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

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

另外请注意,即使对象试图容纳放弃,它仍然可能非常昂贵。 创建一个Microsoft.VisualBasic.Collection(或其他任何名称),添加一些对象,并创建和处理一百万个枚举器。 没问题 – 执行得非常快。 现在创建并放弃一百万个enumeartors。 除非你每隔几千名调查员强制使用一次GC,否则会有大量的贪睡节目。 编写Collection对象是为了允许放弃,但这并不意味着它没有重大成本。

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

谈论终结器 – 正如已经指出的那样,它们有成本,可以通过处理对象(如果它使用SuppressFinalize)来避免。 不仅仅是运行终结器本身的成本,而且还不仅仅是在GC可以收集对象之前必须等到终结器完成的成本。 具有终结器的对象在该集合中存活,其中该对象被识别为未使用且需要完成。 因此它将被提升(如果它还没有在第2代)。 这有几个敲门效果:

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

显然,如果你只是在一个物体上这样做,那么你不可能花费任何明显的成本。 如果你这样做是因为你发现在你使用的对象上调用Dispose,那么它可能会导致上面的所有问题。

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

即使您不关心这种特殊情况,您仍应遵循标准,因为在某些情况下您会关心。 设置一个标准并且总是根据特定的指导方针来遵循它比使用有时无视的标准要容易得多。 随着团队的发展和产品的老化,尤其如此。