什么情况下需要使用dispose()方法?

6
尽管我已经编码了一段时间,但我真的只是一个初级水平的编码者。因此,我了解dispose()的原理,即释放为变量和/或资源保留的内存。我也发现有时使用EF时必须dispose()才能使其他操作正常工作。我不明白的是什么需要释放,何时使用dispose()。
例如,我们不会dispose像string、integer或booleans这样的变量。但是,在某个地方,我们会越过“一条线”,我们使用的变量和/或资源需要被释放。我不明白这条线在哪里。
是否有单一原则或几个广泛的原则可应用于知道何时使用dispose()?
我阅读了这些SO帖子 (a specific situation, more about how rather than when),但我觉得我不理解知道何时使用dispose()的基础知识。我看到的一条评论问当变量超出范围时是否会释放内存,这引起了我的注意,因为直到我看到回复是否定的,它不会仅因为超出范围而被释放,我才会认为它在超出范围时会被释放。虽然我认为这有点苛刻,但我不想成为第二个链接中某个人所说的“无知开发者”。我们中的一些人仍在学习。
因此,我的问题是“什么确定了dispose()何时真正必要?”。
我的问题不是关于如何,而更多关于何时。当然,对如何的评论也很有用,但即使调用dispose()的方法是Using语句,我仍然需要知道何时使用。
编辑原始问题:我知道这是一个长说明,就像“标记为重复”注释请求的那样,这不是发泄,我只是不知道如何确保我将焦点放在我的精确问题上。我们经常因提问方式而被绊倒。正如我在这段长文末尾提到的,一旦我们专注于我的问题,假设我们到达那里,我就会将所有这些都编辑掉。根据我所读的内容,我认为这是一个重要的问题。
提出的“答案”帖子是一个伟大的帖子,但它并没有真正回答我的问题。CodeNotFound在下面的评论中也给出了一个伟大的链接,但它也没有真正回答我的问题。我已经提供了关于这些帖子的评论,以试图帮助精炼我的精确问题。
一次性对象代表着持有有价值资源的对象,而这些资源CLR本身并不知道。很遗憾,我不太理解“CLR本身并不知道的一次性对象”是什么意思。我的问题是:如何知道某个对象是否属于必须要dispose()的范畴?我们经常在代码中定义使用的东西,但什么时候会跨越界限变成需要dispose()的对象呢?顺便说一下,我注意到该帖子的作者从未标记答案。我不知道这是否意味着他认为问题没有得到解答,还是他的后续跟进做得不好,但希望我能更清楚地了解自己想要了解的内容。仔细看答案,它们实际上没有解决“哪些”对象需要开发人员dispose()的问题,或者我如何知道如何识别“哪些”对象。我不知道我创建的对象或事物中哪些需要我负责dispose()。我明白GC和其他规定也会起作用,但那只是“如何”的问题。似乎大多数经验丰富的专业开发人员都知道他们创建的东西何时需要被处理掉。我不明白如何知道“那个”。Proper use of the IDisposable interface: 显然是一个受欢迎的答案(1681个赞),但标记的答案开头是“Dispose的目的是释放非托管资源”。好吧,但我的问题是:如何通过查看某个东西知道它是一个“非托管资源”?我不明白后面的说明与需要dispose()的内容有什么关系。如果您在.NET框架中找到它,则为“托管”。如果您自己在MSDN上搜索,则为“非托管”……现在您需要负责清理它。” 我不知道如何使用这种类型的解释来对需要dispose()的内容进行分类。.net框架中有各种各样的东西,我该如何区分需要dispose()的内容?我要看什么来告诉我我需要负责它?此后,答案继续详细讨论了如何dispose(),但我仍然停留在需要被处理掉的“什么”物品上。为了让这个话题更加复杂,该作者后来说:“所以现在我们将……

  • 摆脱非托管资源(因为我们必须这样做),并且
  • 摆脱托管资源(因为我们想要帮助)
现在我需要考虑处理一整套使用内存的新对象,但我也不知道那些是什么。答案的作者后来说:“对于喜欢这个答案风格(解释为什么,所以如何变得明显)的人...” 我理解作者建议阅读其他文章,但作者认为了解“为什么”会使“如何”变得明显并不是真正合理的,因为对一个人来说明显的东西并不总是对另一个人明显。即使如此,作者更关注“为什么和如何”,而我的问题是关于“何时”,意味着需要dispose()的“什么”,而不是当我完成它时。我知道我什么时候完成事情,我只是不知道我完成它们后要负责哪些事情。对于大多数开发人员来说,需要dispose()的内容可能很明显或本能,但对于像我这样经验有限的人来说并不明显,我希望能够获得关于“什么”的更加专注的对话。确实,“为什么”很有用,但在这种情况下,只有将“为什么”与“什么”联系起来才有用。例如:您必须dispose()一个DbContext,因为CLR不会dispose()它-因为这个“因为”解释了“为什么”,但在这种情况下,DbContext是必须被dispose()的“什么”。我希望有一个通用的原则,可以确定必须被dispose()的“什么”,而不是一长串具体项目的列表,这对于像我这样寻找简单指南的人来说并不特别有用。再次强调,我明白内存释放很重要,也明白需要很多经验和专业知识才能学习“为什么”和“如何”,但我仍然难以理解需要dispose()的“什么”。一旦我理解了我必须dispose()的“什么”,那么我就可以开始努力学习如何做到这一点。所以这还是一个糟糕的问题吗?我稍后会编辑掉所有这些解释,以使帖子更加简洁,假设我们能够更加关注我所问的内容。 最终编辑:虽然我之前说过要编辑掉原来认为不必要的问题文本,但我认为最好保留它。我认为提问的方式有助于我们理解答案。即使答案永远不会改变,如果我们没有将答案与我们在心中构建的问题联系起来,我们可能真正理解不了答案。因此,如果这个问题的提出方式与某人有关联,我鼓励您完全阅读标记为答案的帖子以及评论。尽管最终的答案非常简单,但了解这个问题的答案所需的历史和背景也非常重要。为了清晰起见,在这次有关dispose()的讨论的生命周期中,答案也进行了多次编辑。享受吧...

2
如果您的类型实现了IDisposable接口,请使用Dispose方法。 - Viru
1
Dispose 是一种确定性清理,其中 GC 不是确定性的。 - Daniel A. White
3
你的新问题在什么是.NET中“托管”和“非托管”资源的含义?中得到回答。 :) - CodeCaster
1
所以我理解dispose()的原则,即释放为变量和/或资源保留的内存。但这正是Dispose不适用的地方。它是用于释放需要释放的非托管内存的东西。 - Jon Hanna
1
@Alan:开发人员有一个想要在使用之前详细教授概念的倾向。如果您打算实现IDisposable,则这是必要的(但您可能不需要这样做)。如果您计划使用实现IDisposable的对象,则Eric Lippert的答案就足够了。 - Brian
显示剩余3条评论
4个回答

21
我了解dispose()的原则,它是用于释放变量和/或资源所保留的内存。
您不理解dispose的目的。它并不是为了释放与变量相关联的内存。
当您确定完成使用时,请处理实现IDisposable接口的任何内容。
例如,我们不会处理像字符串、整数或布尔值之类的变量。但是,在某个地方,我们越过了“一个界线”,我们使用的变量和/或资源需要被处理。我不知道这个界线在哪里。
该界限已经划定好了。当对象实现IDisposable接口时,应进行处理。
请注意,变量根本没有被处理。对象被处理。对象不是变量,变量也不是对象。变量是存储值的存储位置。
有单一原则可遵循:当对象是可处理的时请进行处理。
我感觉自己不理解何时使用dispose()的基础知识。
请处理所有可处理的对象。
一个注释问到了当变量超出作用域时是否释放内存,这引起了我的注意,因为在看到回答是不会,变量仅仅因为超出范围而被释放的时候,我曾认为变量会被释放。
请注意语言使用的准确性。您混淆了作用域和生命周期,并将变量与变量的内容混为一谈。
首先:变量的作用域是“程序文本中一个变量可以被名称引用的区域”。变量的生命周期是在程序执行期间变量被视为垃圾收集器的根的时间段。作用域是纯粹的编译时概念,生命周期是纯粹的运行时概念。
作用域和生命周期之间的联系是局部变量的生命周期通常从控制进入变量的范围开始,到离开变量的范围结束。然而,各种事情都可以改变局部变量的生命周期,包括闭包、迭代器块或异步方法中的变量。Jitter优化器还可能缩短或延长局部变量的寿命。请您记住,变量是存储器,并且它可以引用存储器。当局部变量的生命周期结束时,与“局部变量”相关联的存储器可能会被收回。但是,不能保证与“局部变量引用的东西”相关联的存储器在此时或任何时候都会被回收。

因此,我的问题是“什么确定了dispose()何时真正需要?”

如果对象实现了IDisposable,则需要使用Dispose。(有少量可处理但不必处理的对象。例如,任务。但通常情况下,如果它是可处理的,请处理它。)

我的问题不是如何而是何时处理。

只有在完成使用后才处理事物。不早于此时,也不晚于此时。

在.NET中应何时处理对象?

当它们实现IDisposable并且你已经使用它们时,请处理对象。

如何知道某个东西属于我必须处理的范畴?

当它实现IDisposable时。

我不知道自己创建的对象或事物是否需要我负责处理。

需要处理的对象。

大多数经验丰富和专业的开发人员知道他们创建的内容什么时候需要处理。我不知道如何知道这一点。

他们检查对象是否可处理。如果可以,他们就会处理它。

Dispose的重点是释放非托管资源。好的,但我的问题是通过查看某些内容如何知道它是非托管资源?

它实现IDisposable。

我不明白如何使用这种类型的解释来分类需要我dispose()和不需要我dispose()的内容。在.NET框架中有各种各样的东西;我该如何区分需要我负责处理的内容?我应该查看哪些内容来告诉我我要负责它?

请检查是否实现了IDisposable。

之后,答案详细说明了如何dispose(),但我还停留在需要被处理的内容方面。

任何实现了IDisposable的事物都需要被处理。

我的问题是关于“何时”,意思是与“dispose()”相比,“我需要处理什么”,而不是我什么时候完成处理。我知道何时完成处理,只是不知道哪些内容在完成处理后我需要负责处理。

那些实现了IDisposable的东西。

我希望有一个通用原则来说明必须处理哪些内容,而不是一长串具体项的清单,这对像我这样寻求简单指南的人并没有多大用处。

简单的指南是,您应该处理可以处理的物品。

再次强调,释放内存很重要,经验和专业知识也很重要,但我仍然无法理解需要处理哪些内容。一旦我了解了必须处理哪些内容,我就可以开始努力学习如何处理。

通过调用Dispose()处理实现IDisposable接口的内容。

这个问题非常重复。

感谢您以善意的方式接受这个有些愚蠢的答案!

关于作用域和生存期不相等以及变量与对象不同的问题,非常有帮助。这些经常会被混淆,但大多数情况下,这几乎没有区别。但我发现经常挣扎于理解某个概念的人并没有从模糊和不精确中受益。

在Visual Studio中,只需查看对象浏览器/智能感知,以查看对象是否包括Dispose(),绝大多数情况都是这样。但有一些偏远的情况。如我之前所述,TPL团队的共识是,处理任务对象不仅是没有必要的,而且可能会适得其反。

还有一些类型实现了IDisposable,但使用“显式接口实现”技巧使“Dispose”方法仅可以通过强制转换为IDisposable才能访问。在这种情况下,大多数情况下对象本身都有一个同义词用于释放资源,通常称为“Close”或类似的名称。我不太喜欢这种模式,但有些人使用它。

对于这些对象,using块仍将起作用。如果出于某种原因您想明确释放此类对象而不使用using,则可以(1)调用“Close”方法或其他名称,或者(2)将其强制转换为IDisposable并将其处理。

普遍看法是:如果对象是可处理的,则处理它不会有任何损害,并且在您完成后进行处理是一种良好的做法。

原因是:可处理的对象通常代表稀缺的共享资源。例如,文件可能以拒绝其他进程访问该文件的模式打开。确保在完成后尽快关闭文件是一种礼貌的做法。如果一个进程想要使用一个文件,很可能另一个进程很快就会想要使用该文件。

“一次性使用”可能代表图形对象之类的东西。如果在一个进程中活跃的图形对象超过了一万个,操作系统将停止提供新的图形对象,因此在完成任务后,必须释放它们。

关于实现IDisposable,@Brian的评论表明在“正常”的编码情况下,我可能不需要这样做。那么,只有当我的类引入了某些非托管的东西时,我才需要实现IDisposable吗?

好问题。有两种情况需要实现IDisposable。

(1) 常见情况:您正在编写一个长时间持有另一个IDisposable对象的对象,并且“内部”对象的生命周期与“外部”对象的生命周期相同。

例如:您正在实现一个日志记录器类,它打开一个日志文件并保持其打开状态,直到关闭该日志。现在,您有一个持有可释放资源的类,因此它本身也应该是可释放的。

请注意,在这种情况下,“外部”对象无需是可终结的。只需是可释放的。如果由于某种原因没有调用外部对象的dispose,则内部对象的终结器会负责终结。

(2) 罕见情况:您正在实现一个新类,该类要求操作系统或其他外部实体提供必须进行积极清理的资源,并且持有该资源的对象的生命周期与该资源的生命周期相同。

在这种极为罕见的情况下,您应首先问自己是否有任何避免它的方法。对于初学者到中级程序员来说,这是一个不好处理的情况。您真的需要了解CLR如何与非托管代码交互,以便使此问题变得牢固可靠。

如果无法避免,则应尽量不尝试自己实现处理和终结逻辑,特别是如果非托管对象由Windows句柄表示。大多数由句柄表示的操作系统服务应该已经有包装器,但如果没有,则需要仔细研究IntPtr、SafeHandle和HandleRef之间的关系。IntPtr,SafeHandle和HandleRef - 解释

如果您确实需要编写非托管的、非句柄型资源的处理逻辑,并且该资源要求在终结时进行后备,则您将面临重大的工程挑战。

标准的处理模式代码可能看起来很简单,但编写正确的终结逻辑是真正微妙的,而且要在错误情况下保持强大。请记住,终结器在单独的线程上运行,并且在构造函数和线程中止方案的情况下可能与该线程同时运行。编写安全的逻辑可以在在另一个线程上构建对象时清理它,这可能非常困难,我建议不要尝试。

关于编写finalizer的挑战,可以参考我关于这个主题的文章系列:http://ericlippert.com/2015/05/18/when-everything-you-know-is-wrong-part-one/

有一个问题你没有问但我还是会回答:

有没有一些情景下我不应该实现IDisposable接口?

是的。许多人在想要编写以下语义模式时都实现了IDisposable接口:

  • 在世界上做出改变
  • 在新世界里做一些事情
  • 撤销更改

例如:"扮演管理员,执行一些管理任务,恢复为普通用户"。或者 "开始处理事件,在事件发生时执行一些操作,停止处理事件"。或者 "创建一个内存中的错误跟踪器,在可能产生错误的操作后,停止跟踪错误"。等等。你能理解这种模式。

这与可释放模式不符,但这并不能阻止人们编写代表没有未托管资源的类,但仍然像它们有一样实现IDisposable接口。

这个观点使我处于少数派。很多人完全没有问题这种滥用机制。但是当我看到一个可释放对象时,我认为"这个类的作者希望我在我准备好的时候礼貌地清理自己留下的东西。"但实际上这个类的合同通常是"你必须在程序的特定点处处理此对象,如果你不这样做,则剩余的程序逻辑将是错误的,直到你这样做。" 这不是我在看到可释放对象时期望要实现的合同。我期望我可以在方便的时候尽力清理资源。


1
当我第一次看到这个答案时,我本来期望要写一个评论,说如果一个问题需要这么多文字才能回答,那么它应该被关闭为“过于宽泛”。然后我阅读了它... - Servy
为了让它更加复杂,你也可以使用 using 调用 Dispose()。但我理解你为简单起见而省略了它。 - Dorus
1
你的耐心是一种善意。关于作用域!=生命周期和变量!=对象,这非常有帮助。正如你所期望的那样,我仍在努力理解每个人的帮助。有时候重复是最好的老师,我也会笑出声;你已经表达了你的观点,我们处理可丢弃的东西。所以有两个后续问题:(1)在VS中,是否只需查看对象浏览器/智能感知,就可以看到对象是否包含Dispose()?(2)关于实现IDisposable,@Brian的评论表明,在“正常”的编码中,我可能不需要这样做。那么,如果我的类引入了一些非托管的东西,我只需要这样做吗? - Alan
@Alan:我在编辑中回答了你的后续问题。 - Eric Lippert
@Brian:没错。我们已经从事这项工作超过十年了。在业务开发人员需要构建托管包装器以包装非托管对象的情况下,应该早就减少到非常少的数量了。 - Eric Lippert
显示剩余13条评论

1
垃圾回收器(GC)保证在内存限制达到之前释放不再使用的托管内存资源。
让我们来分解一下:
托管:宽泛地说,这意味着完全在.NET/CLR中的资源。例如,由非.NET C++库分配的内存不会被GC释放。
内存:GC仅对内存使用提供保证。存在许多其他资源类型,例如文件句柄。 GC没有逻辑来确保文件句柄被及时释放。
不再使用:这意味着所有引用该内存的变量已经结束了其生命周期。正如Eric Lippert所解释的那样,生命周期!=作用域。
在内存限制达到之前:GC监视“内存压力”并确保在需要时释放内存。它使用一堆算法来决定何时最有效地执行此操作,但重要的是要注意,GC自行决定何时释放资源(对于您的程序来说是不确定的)。
这意味着在以下情况下依赖于垃圾回收是不合适的:
  • 对象引用非托管资源(包括引用引用非托管资源的托管对象)。
  • 对象引用必须释放的除内存之外的资源。
  • 对象引用必须在代码中的特定点(确定性地)释放的资源。
在任何这些情况下,对象应实现IDisposable以处理资源清理。
实例化实现IDisposable的任何对象都必须通过调用Dispose或使用using块来进行清理(它会自动调用Dispose)。

所有与该内存有关的变量都已经结束了它们的生命周期。 - Eric Lippert

1
如果您有一个实现了IDisposable的对象,那么您必须始终在该对象上显式调用.Dispose()。如果它没有实现IDisposable,那么显然您不会调用.Dispose(),因为您不能这样做。
如果您正在编写自己的对象,则是否实现IDisposable的规则很简单:如果您的对象保存对非托管对象的引用或保存对实现IDisposable的对象的引用,则它必须实现IDisposable
GC从不为您调用.Dispose()。您必须始终这样做-直接或通过终结器。
GC 可能(最有可能,但并非总是)调用终结器,因此您可以编写终结器来调用dispose,但要小心正确实现可处置模式,并确保您理解终结器可能永远不会运行,因此如果您的dispose方法执行某些至关重要的操作,则在失去对对象的引用之前直接调用.Dispose()可能更好。

我很想知道为什么这个回答会被踩。我认为我的回答是完全正确的。 - Enigmativity

-1

Dispose模式可用于清除托管和非托管资源。如果您的类中有非托管资源,根据适当的IDisposable实现,您必须同时拥有Dispose和Finalize方法。

我如何知道GC知道/不知道什么?

GC只知道/关心托管对象。GC应该清除那些没有任何强引用的对象。它不知道您的逻辑。举个简单明显的例子。

假设您有一个MainView实例,它持续了很长时间,并创建了另一个LittleView,该视图订阅了MainView实例中的事件。

然后关闭LittleView并使其消失。您知道您不再需要那个LittleView实例了。但是,GC不知道您是否仍然需要LittleView,因为MainWindow中存在活动事件订阅。

因此,GC将不会费心从内存中清除LittleView实例。所以,当您关闭视图时,您应该取消订阅事件。然后GC就知道没有对LittleView的强引用,它是可达的。

该文章还提到,托管资源可能包括非托管资源。哇,这比我最初想象的要深奥得多。我仍然希望能够轻松地识别出需要处理的内容。这是一个复杂的列表吗?需要考虑条件和上下文吗?
没错,托管对象也可以有非托管资源。所有这些托管对象都有其终结方法来清除非托管资源。
你想知道为什么需要在Dispose之外再加上终结方法吗?
非托管资源可能会导致最危险的内存泄漏,因为这样的内存泄漏会一直占用内存,直到重新启动计算机。所以,它们非常糟糕。
假设有一个具有一些非托管资源的托管实例InsA。 InsA已经实现了Dispose方法以清除非托管资源。但是如果您不调用Dispose方法或者忘记调用Dispose方法会发生什么呢?它将永远不会清除那个非托管内存。这就是为什么终结功能被引入的原因。因此,如果您忘记/不调用Dispose,则终结将保证以释放非托管资源的方式执行Dispose。

叹气,又是来自同一MSDN页面的引用:“此接口的主要用途是释放非托管资源。”。就像我说的 - 人们可以选择如何使用“IDisposable”,但让我们记住这里的设计意图。而且说dispose和finalization应该在一起是一个有趣的半陈述dispose模式的说法,依我看来。 - code4life
2
这是一个糟糕的答案。 IDisposable 用于清除未管理的资源。可释放模式中唯一提到“托管”资源的原因是如果释放对象持有对其他 IDisposable 对象的引用。这是遍历可释放对象树以确保所有未管理的资源都被释放的一种方式。GC能够完美地清除受管理的资源,这也是它们被称为“受管理”的原因。 - Enigmativity
另外,对于您的LittleView示例,您只需要删除事件处理程序,以便GC清理该对象。除非该对象实现了IDisposable,否则您不必进行处理。 - Enigmativity
@Enigmativity:“仅当对象实现IDisposable接口时,您才需要执行Dispose操作。”这显然是正确的。 - CharithJ
@Enigmativity: "对于你提到的LittleView示例,你只需要移除事件处理程序,GC就会清理对象。除非对象实现了IDisposable,否则你也不需要进行处理" 再次确认,对于我指示实现的示例,需要实现IDisposable。 - CharithJ
显示剩余4条评论

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