避免处理系统定义的笔和画刷实例。

11

据我所知,最佳实践是在使用PenBrush的实例时调用Dispose()方法,但若它们已被设置为系统预定义值(例如System.Drawing.BrushesSystem.Drawing.PensSystem.Drawing.SystemBrushes),则无需这样做。

试图释放系统定义的资源会导致抛出异常。

除了将Dispose()调用包装在try/catch中之外,似乎不太容易检测到这些资源是引用系统定义的还是用户定义的值。

8个回答

34

这是一个旧问题,我知道,但是我一直在研究涉及GDI对象的资源泄漏,并且几乎接受的答案中的每个声明都是错误的。为了确保以后通过搜索找到此问题的读者的准确性,就像我一样:

没有必要调用Dispose。

这种说法是误导性的。虽然技术上您可以不调用Dispose,但是不处理自己拥有的画笔或钢笔是一种非常糟糕的实践。

原因是:进程中可以使用的GDI对象数量有一个硬限制(默认设置为一万,尽管可以通过注册表修改增加),垃圾收集器可能无法在资源泄漏时更快地完成资源的终结。 管理的内存分配产生收集压力,但没有相应的终结压力

垃圾回收的目的是消除这些要求。

不是这样的。垃圾集合的目的是模拟具有任意多内存的环境。

IDisposable 的主要目的之一是允许类在资源有限的环境中清理非托管资源。

这是正确的。

如果您没有调用Dispose()方法,则该类的非托管资源将在垃圾回收期间对象被终结和释放时得到清理。

这有时是正确的;对于GDI对象,这是正确的。对于持有非托管资源的类来说,实现终止语义作为后备措施是一种好的实践;这是为遵循接受的答案中给出的错误建议的人提供的额外保护层。您不应该依赖终结器来拯救您的错误;您应该处理自己的资源。

只有在疯狂的例外情况下才应依赖终结器。例如:

Brush myBrush = MakeMeABrush();
DoSomethingWithBrush(myBrush);
myBrush.Dispose();
假设在线程分配了笔刷之后,但在将笔刷赋值给myBrush之前,引发了线程中止异常。无论如何使用try-catch-finally的组合,都无法通过Dispose清理那支笔刷;你必须依靠终结器。这就是终结器存在的原因:在完全疯狂的情况下,你无法自我处理的异常情况。 这不是懒散的借口

如果您“必须”调用dispose并且不知道brush实例是“系统 brush”还是“普通brush”,那么就必须使用try...catch块。

虽然从技术上讲这是正确的,但它完全忽略了问题的关键。如果您处于不知道是否拥有该笔刷的情况下,则程序设计中存在错误。不要用try-catch块掩盖错误!解决问题!
这种情况对所有明确管理资源的情况都很常见:提供资源的实体和使用资源的实体负责清楚地记录哪个实体拥有清理资源。如果您不知道是否拥有给定的笔刷,那么 有人没有履行其责任,即防止这种情况发生
如果您决定的契约是提供资源的实体负责稍后清理它,则您的使用者根本不应处置该笔刷,因为这会违反合同。如果需要,生产者将处理清理。
如果您决定的契约是生产者和消费者都将释放资源,则消费者必须对传入的每个笔刷调用Clone,以确保它们拥有可安全处理的资源,并且生产者仍然拥有有效的资源。

如果你选择的合同是消费资源的实体负责稍后清理它,那么服务提供商需要始终为您提供一个可以安全处理的画笔,并且不能自行处理该资源。由于提供程序知道他们是自己制作的还是从系统中获取的画笔,因此提供程序必须在系统画笔上调用Clone()以获得可以安全处理的画笔,然后将传递给消费者。

但是“没有人打扫并且我们希望一切顺利”的合同非常糟糕。

不需要对SystemBrushesSystemPens调用Dispose(),因为GDI+库会处理这些资源。

这种解释实际上并没有解释任何事情。之所以禁止处理其中之一的画笔,是因为画笔的生命周期等于应用程序域的生命周期

类的备注部分会注明是否需要调用Dispose方法。

这个说法是错误的。您应该假设除非您有充分的理由相信不需要处理,否则始终需要Dispose一个IDisposable资源。文档中缺少一行并不意味着处理不必要。

为了以积极的姿态结束:

如果我有一个图形密集型应用程序或类,那么我将缓存所需的笔和画笔实例。我在整个应用程序或类的生命周期内使用它们。

这是一个好的做法。如果创建的画笔和笔刷数量相对较少,并且一直在重复使用相同的画笔和笔刷,则将它们永久缓存是很有意义的。由于它们的生命周期是到程序结束为止,所以不需要处理它们。在这种情况下,我们不会处理垃圾,因为它不是垃圾,而是有用的。当然,没有被缓存的GDI对象应该仍然被处理。再次强调,追求缓存策略并不是进行不良实践的借口。


2
@supercat:你关于.Font赋值不会触发处理的说法是错误的。Hans Passant在回答https://dev59.com/IlTTa4cB1Zd3GeqPvcRu时讨论了这个问题。检查.Net参考源代码中的Control.cs可以证实Hans是正确的;在`OnFontChanged`函数和`Font` setter中都有代码来确保Font被处理。这些dispose调用并不总是发生(例如,如果你将一个字体分配给自己,它不会被处理,因为显然正在使用)。 - Brian
2
@AMissico:缺少编译器错误是因为在一般情况下,编译器无法知道哪个对象引用将最后使用。如果IDisposable包含在框架的核心中,而不是作为事后想法,这些问题将得到更好的处理。然而,允许实现IDisposable的对象被放弃而没有调用Dispose的代码(除非有充分的理由),应被视为有缺陷的。 - supercat
1
@AMissico:你对编译器的信任虽然令人感动,但完全是错误的。想一想:如果有一种方法可以让编译器知道何时应该处理对象,那么也会有一种方法让运行时知道,因为运行时比编译器拥有更多信息。由于按照定义,我们需要IDisposable来处理那些资源必须手动清理,因为运行时无法很好地完成清理工作的情况,显然,编译器无法告诉您何时应该处理。如果它可以这样做,那么我们就不需要IDisposable了! - Eric Lippert
4
@AMissico: 你也在深度不幸地使用“requirement(需求)”这个词。你基本上在说程序员没有要求选择使用最佳实践来编写程序;一个程序员可以选择编写松散,危险,脆弱的代码。从某种意义上讲,这是正确的;迄今为止还没有发明出防止开发人员编写糟糕代码的编程语言。但是,基于“你可以逃脱惩罚”,去倡导最差的做法会给你的同行程序员带来损害。 - Eric Lippert
4
@AMissico:那么你也许应该学会欣赏建设性的批评,而不是反感;这样做对我的职业生涯有很大帮助,也可能对你有所帮助。你认为“差不多就好”和细节不重要的态度也需要调整。我并不是因为你的回答不完美而进行批评,而是因为它正在积极地伤害这个世界。它可以在网上轻易地被找到,并包含一些非常糟糕的建议,那些不了解更好方式的人——如果他们知道更好的方式,他们为什么还要寻求建议呢?——可能会采纳。 - Eric Lippert
显示剩余16条评论

5
当你完成创建一个Graphics对象的操作时,必须通过调用它的Dispose方法来处理它,以销毁该对象。(对于许多不同的GDI+对象都适用这一规则。)不要将其保留到下次使用,否则它将无效。在完成使用后,你必须完全处理掉Graphics对象。如果不这样做,可能会导致图像损坏、内存使用问题或更糟糕的是国际冲突。所以,请正确地处理掉所有Graphics对象。
如果你在事件中创建了一个Graphics对象,务必在退出该事件处理程序之前销毁它。不能保证Graphics对象在稍后的事件中仍然有效。此外,重新创建另一个Graphics对象也很容易。
获取信息来源: http://msdn.microsoft.com/en-us/library/orm-9780596518431-01-18.aspx

2
首先,您应该尽可能地处理刷子并且不要将其留给垃圾回收器。虽然 GDI 最终会处理这些东西(假设库被正确关闭),但无法预测何时会发生。实际上,我了解到刷子句柄会长期存在。在长时间运行的应用程序中,您将面临一个事实上的内存泄漏。当然,在小型应用程序中或仅在极少数情况下创建笔刷的应用程序中,您可以让系统处理它,但我认为这是粗心的做法。
通常情况下,每当我创建笔刷时,我都会在 using 语句中进行操作。这将自动调用笔刷的 Dispose 而无需担心它。作为额外的奖励,由于您在语句中创建笔刷,因此您知道它不是预定义的笔刷。任何时候创建和使用非预定义笔刷,请将块包装在 using 中,您根本不需要担心它。实际上,即使出现异常,该块也会自动调用 Dispose。

2

简短的回答是,如果您创建了它,请委托负责清理或自己清理对象。如果让东西挂在垃圾收集器中,就可能会创建GDI资源“泄漏”。它们最终可能会被清除,但是挂在那里没有任何好处。例如,如果不在打开的文件上调用“close”或Dispose,则文件将保持锁定状态,直到GC“处理完它为止”。


0

我可能错了,但我认为您可以假设预定义的画笔和笔刷的生命周期(以及处理)不是您应用程序的责任,而将由系统处理。

简而言之:不要在预定义的内容上调用Dispose。 :)


是的,那就是我想要避免的 - 我的问题是如何知道调用Dispose()是否可行。我想知道是否还有比在try / catch内调用Dispose()更好的方法,因为我宁愿避免抛出异常带来的“代价”。 - David Gardiner
@谁谁谁:为什么要踩?Lzcd 是正确的。你可以通过微软的源代码或 .NET Reflector 进行验证。 - AMissico

0

唯一想到的是练习时不要将系统笔刷作为方法参数。


或者制定一个政策:如果调用者传递了一个对象,则应适当地调用 Dispose()。如果调用者获取了该对象,则知道它是从系统画刷获取的还是其他,可以相应地包含或省略 using 块。 - binki

0
仅为完整性考虑,使用反射器我发现System.Drawing.Pen和System.Drawing.SolidBrush类有一个名为“immutable”的私有字段。
当对象引用系统定义的资源时,此字段似乎设置为true,因此您可以使用反射来仔细检查此字段的值,以决定是否调用Dispose()。

-9

没有必要调用Dispose方法。垃圾回收的目的是为了消除这些要求。

IDisposable的主要目的之一是允许类在资源有限的环境中清理非托管资源。如果您不调用dispose方法,则在对象被终结和在垃圾回收期间处理时,该类的非托管资源将被清理。

如果您“必须”调用dispose并且不知道刷子实例是“系统”还是“普通”刷子,则必须使用try...catch块。

  • 不需要对SystemBrushesSystemPens调用dispose,因为GDI+库会处理这些资源。
  • 可以对SystemFontsSystemIcons进行处理。

类的备注部分将说明是否需要调用Dispose方法。如果备注部分建议调用Dispose方法,则我会这样做。

通常情况下,我不会对笔和画刷调用dispose方法。如果我有一个图形密集型的应用程序或类,那么我会缓存我需要的笔和画刷实例。我在整个应用程序或类的生命周期中都会使用它们。如果我不这样做,那么图形绘制性能将受到影响,因为尝试创建和处理所有这些对象的次数太多且太频繁。(嗯...现在我想起来了,性能可能就是我们不能处理SystemBrushes和SystemPens,但可以处理SystemFonts和SystemIcons的原因。甚至框架也缓存SystemBrushes和SystemPens。)

1
感谢您的评论,不过我认为需要澄清的是,只有在声明了终结器时GC才会清理非托管资源 - 它不会自动调用Dispose()方法。应用程序可以通过调用Dispose()来预先处理GC,这样它就可以更快地被回收。否则,如果没有其他方法知道画刷/笔是设置为什么,我认为您是正确的,使用try/catch是唯一的选择。 - David Gardiner
请注意,几乎所有基于GDI+的类都实现了IDisposable接口,因为它们使用非托管资源。因此,在这个问题和答案中,Dispose(disposing=False)将始终通过Finalize方法调用。尽管如此,仍然没有必要调用Dispose - AMissico
当GDI+库关闭时,该框架会自动调用任何已创建的SystemBrushesSystemPens对象的Dispose方法。 - AMissico
我没有使用“自动”这个词。我省略了细节,因为这不是问题/答案的一部分。 - AMissico
很遗憾,这种情况下的注释是不完整的。请查看文档中的示例代码--它确实调用了Dispose,就像应该做的那样。实际上,第一条语句作为一般规则是有效的。如果注释如此完整,我们是否还需要像这个网站这样的站点呢? :) - Ari Roth
2
@DavidGardiner:你会考虑取消接受这个答案吗,这样它就不会再被固定在页面顶部,等待误导更多读者了吗? - Joren

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