简体   繁体   English

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

[英]Why is it always necessary to implement IDisposable on an object that has an IDisposable member?

From what I can tell, it is an accepted rule that if you have a class A that has a member m that is IDisposable, A should implement IDisposable and it should call m.Dispose() inside of it.据我所知,如果你有一个 class A 有一个成员 m 是 IDisposable,A 应该实现 IDisposable 并且它应该在其中调用 m.Dispose() 是一个公认的规则。

I can't find a satisfying reason why this is the case.我找不到令人满意的理由。

I understand the rule that if you have unmanaged resources, you should provide a finalizer along with IDisposable so that if the user doesn't explicitly call Dispose, the finalizer will still clean up during GC.我理解如果你有非托管资源,你应该提供一个终结器以及 IDisposable 的规则,这样如果用户没有显式调用 Dispose,终结器仍然会在 GC 期间清理。

However, with that rule in place, it seems like you shouldn't need to have the rule that this question is about.但是,有了该规则,您似乎不需要拥有该问题所涉及的规则。 For instance...例如...

If I have a class:如果我有 class:

class MyImage{
  private Image _img;
  ... }

Conventions states that I should have MyImage: IDisposable .公约规定我应该有MyImage: IDisposable But if Image has followed conventions and implemented a finalizer and I don't care about the timely release of resources, what's the point?但是如果 Image 遵循约定,实现了一个 finalizer,而我不关心资源的及时释放,那还有什么意义呢?

UPDATE更新

Found a good discussion on what I was trying to get at here .找到了关于我想在这里得到什么的很好的讨论。

But if Image has followed conventions and implemented a finalizer and I don't care about the timely release of resources, what's the point?但是如果 Image 遵循约定,实现了一个 finalizer,而我不关心资源的及时释放,那还有什么意义呢?

You've missed the point of Dispose entirely.您完全错过了 Dispose 的要点。 It's not about your convenience.这不是为了您的方便。 It's about the convenience of other components that might want to use those unmanaged resources.这是关于可能想要使用这些非托管资源的其他组件的便利性。 Unless you can guarantee that no other code in the system cares about the timely release of resources, and the user doesn't care about timely release of resources, you should release your resources as soon as possible.除非你能保证系统中没有其他代码关心资源的及时释放,而用户不关心资源的及时释放,否则你应该尽快释放你的资源。 That's the polite thing to do.这是礼貌的做法。

In the classic Prisoner's Dilemma , a lone defector in a world of cooperators gains a huge benefit.在经典的囚徒困境中,合作者世界中的一个孤独的叛逃者获得了巨大的利益。 But in your case, being a lone defector produces only the tiny benefit of you personally saving a few minutes by writing low-quality, best-practice-ignoring code.但在你的情况下,作为一个孤独的叛逃者只会为你个人节省几分钟的时间带来微小的好处,因为你可以编写低质量、忽略最佳实践的代码。 It's your users and all the programs they use that suffer, and you gain practically nothing.受苦的是您的用户和他们使用的所有程序,而您几乎一无所获。 Your code takes advantage of the fact that other programs unlock files and release mutexes and all that stuff.您的代码利用了其他程序解锁文件并释放互斥锁和所有这些东西的事实。 Be a good citizen and do the same for them.做一个好公民,为他们做同样的事。 It's not hard to do, and it makes the whole software ecosystem better.做起来并不难,它让整个软件生态系统变得更好。

UPDATE: Here is an example of a real-world situation that my team is dealing with right now.更新:这是我的团队现在正在处理的真实情况的示例。

We have a test utility.我们有一个测试实用程序。 It has a "handle leak" in that a bunch of unmanaged resources aren't aggressively disposed;它有一个“句柄泄漏”,因为一堆非托管资源没有被积极处置; it's leaking maybe half a dozen handles per "task".每个“任务”可能会泄漏六个句柄。 It maintains a list of "tasks to do" when it discovers disabled tests, and so on.当它发现禁用的测试等时,它会维护一个“要执行的任务”列表,等等。 We have ten or twenty thousand tasks in this list, so we very quickly end up with so many outstanding handles -- handles that should be dead and released back into the operating system -- that soon none of the code in the system that is not related to testing can run.我们在这个列表中有一两万个任务,所以我们很快就得到了这么多未完成的句柄——应该死掉并释放回操作系统的句柄——很快系统中没有一个不是相关测试可以运行。 The test code doesn't care.测试代码不在乎。 It works just fine.它工作得很好。 But eventually the code being tested can't make message boxes or other UI and the entire system either hangs or crashes.但最终被测试的代码无法制作消息框或其他 UI,整个系统要么挂起,要么崩溃。

The garbage collector has no reason to know that it needs to run finalizers more aggressively to release those handles sooner;垃圾收集器没有理由知道它需要更积极地运行终结器以更快地释放这些句柄; why should it?为什么要呢? Its job is to manage memory.它的工作是管理 memory。 Your job is to manage handles, so you've got to do that job.您的工作是管理句柄,因此您必须完成这项工作。

But if Image has followed conventions and implemented a finalizer and I don't care about the timely release of resources, what's the point?但是如果 Image 遵循约定,实现了一个 finalizer,而我不关心资源的及时释放,那还有什么意义呢?

Then there isn't one, if you don't care about timely release, and you can ensure that the disposable object is written correct (in truth I never make an assumption like that, not even with MSs code. You never know when something accidentally slipped by).那么没有一个,如果你不关心及时发布,你可以确保一次性 object 写的正确(事实上我从来没有做过这样的假设,即使是 MSs 代码。你永远不知道什么时候不小心滑过)。 The point is that you should care, as you never know when it will cause a problem.关键是你应该关心,因为你永远不知道它什么时候会导致问题。 Think about an open database connection.考虑一个开放的数据库连接。 Leaving it hanging around, means that it isn't replaced in the pool.让它闲置,意味着它不会在池中被替换。 You can run out if you have several requests come in for one.如果您有多个请求进入一个,您可能会用完。

Nothing says you have to do it if you don't care.如果你不在乎,没有什么说你必须这样做。 Think of it this way, it's like releasing variables in an unmanaged program.这样想,就像在非托管程序中释放变量一样。 You don't have to, but it is highly advisable.您不必这样做,但这是非常可取的。 If for no other reason the person inheriting from the program doesn't have to wonder why it wasn't taken care of and then try and clear it up.如果没有其他原因,从程序继承的人不必想知道为什么它没有得到照顾,然后尝试清理它。

Firstly, there's no guaranteeing when an object will be cleaned up by the finalizer thread - think about the case where a class has a reference to a sql connection.首先,无法保证 object 何时会被终结器线程清理 - 想想 class 引用 sql 连接的情况。 Unless you make sure this is disposed of promptly, you'll have a connection open for an unknown period of time - and you won't be able to reuse it.除非您确保及时处理,否则您将打开一个未知时间段的连接 - 您将无法重复使用它。

Secondly, finalization is not a cheap process - you should be making sure that if your objects are disposed of properly you're calling GC.SuppressFinalize(this) to prevent finalization happening.其次,终结不是一个廉价的过程——你应该确保如果你的对象被正确处理,你调用 GC.SuppressFinalize(this) 来防止终结的发生。

Expanding on the "not cheap" aspect, the finalizer thread is a high-priority thread.扩展“不便宜”方面,终结器线程是一个高优先级线程。 It will take resources away from your main application if you give it too much to do.如果你给它做太多事情,它会从你的主应用程序中夺走资源。

Edit: Ok, here's a blog article by Chris Brummie about Finalization , including why it is expensive.编辑:好的,这是 Chris Brummie 的一篇关于Finalization的博客文章,包括为什么它很贵。 (I knew I'd read loads about this somewhere) (我知道我在某处读过很多关于这个的文章)

If you don't care about the timely release of resources, then indeed there is no point.如果你不关心资源的及时释放,那确实没有意义。 If you can be sure that the code is only for your consumption and you've got plenty of free memory/resources why not let GC hoover it up when it chooses to.如果您可以确定代码仅供您使用,并且您有大量可用内存/资源,为什么不让 GC 在它选择时将其吸走。 OTOH, if someone else is using your code and creating many instances of (eg) MyImage , it's going to be pretty difficult to control memory/resource usage unless it disposes nicely. OTOH,如果其他人正在使用您的代码并创建(例如) MyImage的许多实例,那么控制内存/资源使用将非常困难,除非它处理得很好。

Many classes require that Dispose be called to ensure correctness.许多类要求调用 Dispose 以确保正确性。 If some C# code uses an iterator with a "finally" block, for example, the code in that block will not run if an enumerator is created with that iterator and not disposed.例如,如果某些 C# 代码使用带有“finally”块的迭代器,则如果使用该迭代器创建枚举器并且未释放该枚举器,则该块中的代码将不会运行。 While there a few cases where it would be impractical to ensure objects were cleaned up without finalizers, for the most part code which relies upon finalizers for correct operation or to avoid memory leaks is bad code.虽然在少数情况下确保在没有终结器的情况下清理对象是不切实际的,但对于大多数依赖终结器来正确操作或避免 memory 泄漏的代码是错误的代码。

If your code acquires ownership of an IDisposable object, then unless either the object's cleass is sealed or your code creates the object by calling a constructor (as opposed to a factory method) you have no way of knowing what the real type of the object is, and whether it can be safely abandoned.如果您的代码获得了 IDisposable object 的所有权,那么除非对象的 cleass 被密封或您的代码通过调用构造函数(而不是工厂方法)创建 object,否则您无法知道 ZA8Z6FDE6331BD48B666AC 的真实类型是什么,以及是否可以安全地放弃。 Microsoft may have originally intended that it should be safe to abandon any type of object, but that is unrealistic, and the belief that it should be safe to abandon any type of object is unhelpful. Microsoft 最初可能打算放弃任何类型的 object 应该是安全的,但这是不现实的,并且认为放弃任何类型的 object 应该是安全的信念是没有帮助的。 If an object subscribes to events, allowing for safe abandonment will require either adding a level of weak indirection to all events, or a level of (non-weak) indirection to all other accesses.如果 object 订阅事件,则允许安全放弃将需要为所有事件添加一个弱间接级别,或者为所有其他访问添加一个(非弱)间接级别。 In many cases, it's better to require that a caller Dispose an object correctly than to add significant overhead and complexity to allow for abandonment.在许多情况下,最好要求调用者正确处理 object,而不是增加大量开销和复杂性以允许放弃。

Note also, btw, that even when objects try to accommodate abandonment it can still be very expensive.另请注意,顺便说一句,即使对象试图适应遗弃,它仍然可能非常昂贵。 Create a Microsoft.VisualBasic.Collection (or whatever it's called), add a few objects, and create and Dispose a million enumerators.创建一个 Microsoft.VisualBasic.Collection(或其他任何名称),添加一些对象,然后创建和处置一百万个枚举器。 No problem--executes very quickly.没问题——执行非常快。 Now create and abandon a million enumeartors.现在创建并放弃一百万个枚举器。 Major snooze fest unless you force a GC every few thousand enumerators.除非你每隔几千个枚举器就强制执行一次 GC,否则会打盹。 The Collection object is written to allow for abandonment, but that doesn't mean it doesn't have a major cost.收集 object 是为了允许放弃而编写的,但这并不意味着它没有很大的成本。

If an object you're using implements IDisposable, it's telling you it has something important to do when you're finished with it.如果您使用的 object 实现了 IDisposable,它会告诉您在完成后它还有重要的事情要做。 That important thing may be to release unmanaged resources, or unhook from events so that it doesn't handle events after you think you're done with it, etc, etc. By not calling the Dispose, you're saying that you know better about how that object operates than the original author.重要的事情可能是释放非托管资源,或者从事件中解开挂钩,以便在您认为完成之后它不会处理事件等等。通过不调用 Dispose,您是在说您知道得更好关于 object 的操作方式比原作者。 In some tiny edge cases, this may actually be true, if you authored the IDisposable class yourself, or you know of a bug or performance problem related to calling Dispose.在一些微小的边缘情况下,如果您自己编写了 IDisposable class,或者您知道与调用 Dispose 相关的错误或性能问题,这实际上可能是正确的。 In general, it's very unlikely that ignoring a class requesting you to dispose it when you're done is a good idea.一般来说,忽略 class 要求您在完成后处理它的可能性很小。

Talking about finalizers - as has been pointed out, they have a cost, which can be avoided by Disposing the object (if it uses SuppressFinalize).谈论终结器 - 正如已经指出的那样,它们有成本,可以通过处理 object(如果它使用 SuppressFinalize)来避免。 Not just the cost of running the finalizer itself, and not just the cost of having to wait till that finalizer is done before the GC can collect the object.不仅仅是运行终结器本身的成本,也不仅仅是在 GC 收集 object 之前必须等到终结器完成的成本。 An object with a finalizer survives the collection in which it is identified as being unused and needing finalization.带有终结器的 object 在被识别为未使用且需要终结器的集合中幸存下来。 So it will be promoted (if it's not already in gen 2).所以它将被提升(如果它还没有在第 2 代中)。 This has several knock on effects:这有几个连锁反应:

  • The next higher generation will be collected less frequently, so after the finalizer runs, you may be waiting a long time before the GC comes around to that generation and sweeps your object away.下一个更高代的收集频率将降低,因此在终结器运行后,您可能要等待很长时间,然后 GC 才会到达该代并将您的 object 扫走。 So it can take a lot longer to free memory.因此释放 memory 可能需要更长的时间。
  • This adds unnecessary pressure to the collection the object is promoted to.这给 object 提升到的集合增加了不必要的压力。 If it's promoted from gen 0 to gen 1, then now gen 1 will fill up earlier than it needs to.如果它从 0 代提升到 1 代,那么现在 1 代将比它需要的更早填满。
  • This can lead to more frequent garbage collections at higher generations, which is another performance hit.这可能会导致更高代的垃圾 collections 更频繁,这是另一个性能损失。
  • If the object's finalizer isn't completed by the time the GC comes around to the higher generation, the object can be promoted again.如果在 GC 到达更高代时对象的终结器尚未完成,则可以再次提升 object。 Hence in a bad case you can cause an object to be promoted from gen 0 to gen 2 without good reason.因此,在不好的情况下,您可能会导致 object 从第 0 代提升到第 2 代,而没有充分的理由。

Obviously if you're only doing this on one object it's not likely to cost you anything noticeable.显然,如果您只在一个 object 上执行此操作,则不太可能花费您任何明显的费用。 If you're doing it as general practice because you find calling Dispose on objects you're using tiresome, then it can lead to all of the problems above.如果您因为发现对正在使用的对象调用 Dispose 感到厌烦而将其作为常规做法,那么它可能会导致上述所有问题。

Dispose is like a lock on a front door. Dispose 就像前门上的锁。 It's probably there for a reason, and if you're leaving the building, you should probably lock the door.它的存在可能是有原因的,如果你要离开大楼,你可能应该锁上门。 If it wasn't a good idea to lock it, there wouldn't be a lock.如果锁定它不是一个好主意,就不会有锁。

Even if you don't care in this particular case, you should still follow the standard because you will care in some cases.即使您在这种特殊情况下不关心,您仍然应该遵循标准,因为在某些情况下您会关心。 It's much easier to set a standard and follow it always based on specific guidelines than have a standard that you sometimes disregard.制定一个标准并始终根据特定的指导方针遵循它比制定一个您有时无视的标准要容易得多。 This is especially true as your team grows and your product ages.随着您的团队成长和产品老化,尤其如此。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM