“Dispose” 方法只适用于包含非托管资源的类型吗?

68

最近我和同事就实现Dispose和实现IDisposable接口的类型的价值进行了讨论。

我认为对于应该尽快清理的类型,即使没有未受控资源需要清理,实现IDisposable也是有价值的。

我的同事持不同意见。他认为如果没有未受控资源,实现IDisposable是不必要的,因为您的类型最终会被垃圾回收。

我的观点是,如果您想尽快关闭ADO.NET连接,则实现IDisposable并使用using new MyThingWithAConnection()是有意义的。 我的同事回答说,在内部,ADO.NET连接是一个未受控资源。而我回复他的回答是最终所有东西都是未受控资源

我知道有推荐的可释放模式,在这种模式下,如果调用Dispose,则释放托管和未托管资源,但只释放未受控资源如果通过终结器/destructor调用它(并且一段时间以前我在博客中分享了如何警告使用您的IDisposable类型的不当方式的消费者)。

那么,我的问题是,如果您有一个不包含未受控资源的类型,是否值得实现IDisposable


2
正如您正确指出的那样,ADO连接一种非托管资源。 - Konrad Rudolph
1
@KonradRudolph - 不是。连接被称为“托管”资源。它包含(拥有)一个非托管资源,但可能是通过SafeHandle间接拥有的。 - H H
@Henk 这就是我的意思 - 我应该更仔细地措辞,但在问题中已经以正确的方式表达了。 - Konrad Rudolph
2
我曾经需要使用 IDisposable 的唯一另一个场合,是在处理非托管资源之外,当我需要确保事件被正确取消订阅以便类可以被垃圾回收时。但这实际上是语言的缺陷:事件真的真的 非常 需要弱引用,但它们并没有。 - BlueRaja - Danny Pflughoeft
15个回答

36
有不同的有效用途 IDisposable。一个简单的例子是持有打开的文件,你需要在某个时刻关闭它,只要你不再需要它了。当然,你可以提供一个方法Close,但在 Dispose内使用using (var f = new MyFile(path)) { /*process it*/ }这样的模式会更可靠避免异常。

一个更流行的例子是持有其他一些需要释放的IDisposable资源,这通常意味着您需要提供自己的Dispose以便将它们释放。

通常,只要您想要确定性地销毁任何东西,您就需要实现IDisposable

我的观点与你的观点不同,在我看来只有当某种资源需要确定性的销毁/释放时,我才会实现IDisposable,而不是尽快实现。在这种情况下,依赖垃圾回收并不是一个选择(与你的同事的说法相反),因为它发生在不可预测的时间,并且实际上可能根本不会发生!

任何资源都是不受管理的事实并没有什么意义:开发人员应该考虑“何时以及如何释放此对象”而不是“它在底层如何工作”。底层实现可能随时间变化而改变。

事实上,C#和C ++之间的主要区别之一是缺少默认的确定性销毁。 IDisposable填补了这个空缺:您可以命令确定性销毁(尽管您不能确保客户端调用它;同样,在C ++中,您无法确保客户端调用delete)。


小补充:确定性释放资源与尽快释放资源之间实际上有什么区别? 实际上,这些是不同的(尽管并非完全正交的)概念。

如果要确定地释放资源,这意味着客户端代码应该有可能说“现在我想释放这个资源”。这实际上可能不是资源可以被释放的最早时间:持有资源的对象可能已经从资源中获取了所有需要的内容,因此它可能已经自行释放了该资源。另一方面,该对象可能选择保留(通常未受管理的)资源,即使在对象的Dispose方法运行后,只在终结器中清理它(如果长时间持有资源不会产生任何问题)。
所以,为了尽快释放资源,严格来说,并不需要Dispose方法:一旦它自己意识到不再需要该资源,对象就可以立即释放该资源。然而,Dispose方法提供了一个有用的提示,即对象本身不再需要,因此如果合适的话,可能会在那时释放资源。
还有一个必要的补充:并不只有未受管理的资源需要确定性地分配!这似乎是答案之间观点差异的关键之一。人们可以构建纯想象的结构,这些结构可能需要确定性地释放。
例如:访问某些共享结构的权限(考虑RW-lock),大量存储器块(想象一下你手动管理程序的一些内存),许可证可使用其他程序(想象一下你不允许同时运行X个某个程序副本),等等。在这里,要释放的对象不是未受管理的资源,而是做/使用某些东西的权利,这是纯粹的程序逻辑内部构建。

小小的补充:这里有一些巧妙地滥用IDisposable的示例列表:http://www.introtorx.com/Content/v1.0.10621.0/03_LifetimeManagement.html#IDisposable


2
除了文件和数据库连接等非托管资源之外,您何时需要确定性销毁?(还有一个小问题-对象本身并没有被确定性地销毁,只有它使用的资源...而且仅当这些资源是非托管的时候) - BlueRaja - Danny Pflughoeft
@BlueRaja-DannyPflughoeft:请看我的回答,但直接回答你的问题:1)当你通过实现“IDisposable”的其他对象间接使用这些资源时,或者当因某种原因“IDisposable”习惯用法(以及“using”块的便利性)是有用的时。 - Adam Robinson
@BlueRaja:任何资源都可能需要确定性的释放。它可能是任务表中的一个插槽,巨大的内存块,协作工作者环形队列中的令牌,或其他任何东西。 - Vlad
@supercat:释放锁并回收其资源并不是同一件事。锁和强事件都不需要确定性的销毁。 - BlueRaja - Danny Pflughoeft
2
@BlueRaja-DannyPflughoeft:对于一个被锁定的对象,独占访问权本身就是一种资源;如果持有独占访问权的对象在没有通知被保护对象不再需要排他性访问的情况下被丢弃,那么就没有人能够再使用该对象了。至于事件,如果有数量不限的短暂对象订阅来自长期存在的对象的事件,但很快就被丢弃,这些对象将创建一个无限制的内存泄漏,因为它们在长期存在的对象的生命周期内不会变得可回收。 - supercat
显示剩余3条评论

17

我认为最有帮助的是将IDisposable视为责任。 如果一个对象知道在它不再需要和宇宙结束之间(最好尽快),需要完成某些任务,并且如果它是唯一具有信息和动力的对象,则应该实现IDisposable。 例如,打开文件的对象有责任确保文件被关闭。 如果该对象消失而不关闭文件,则该文件可能在任何合理时间内都无法关闭。

重要的是要注意,即使仅与100%托管对象交互的对象也可能需要进行清理(并应使用IDisposable)。 例如,附加到集合的“修改”事件的IEnumerator将需要在不再需要时分离自身。 否则,除非枚举器使用一些复杂的技巧,否则只要集合在作用域内,枚举器就永远不会被垃圾回收。 如果集合被枚举一百万次,则会向其事件处理程序附加一百万个枚举器。

请注意,在某些情况下,可能可以在未调用Dispose的情况下使用终结器进行清理。 有时这很有效; 有时它会非常糟糕。 例如,即使Microsoft.VisualBasic.Collection使用终结器将枚举器从“修改”事件中分离,但在没有介入的Dispose或垃圾回收的情况下尝试多次枚举此类对象会使其变得非常缓慢-比正确使用Dispose导致的性能慢几个数量级。


1
谢谢,我希望我能想到IEnumerator的例子作为反驳! - Steve Dunn
2
@SteveDunn:谢谢。似乎有一种普遍的观念,即“未管理资源”中的术语“未管理”与“未管理代码”有某种关系。现实情况是,这两个概念在很大程度上是正交的。Finalizers 会在某种程度上混淆“清理责任”问题,因为没有它们,“在宇宙的末日之前”的措辞可能会有些字面意义。如果一个没有 finalizer 的对象持有授予对某物独占访问权限的句柄的唯一副本,并且在没有释放句柄的情况下被抛弃,则该句柄将永远不会被释放。 - supercat
1
当然,无论句柄是否被释放的问题,在宇宙末日之前都可能变得不重要,但关键点是,一旦所有副本都消失了,就再也没有什么能够释放该句柄了。因此,最后一个拥有其副本的实体必须确保在该句柄的副本仍然存在时将其释放。另外,锁也是完全在托管代码内的“非托管资源”的另一个很好的例子。 - supercat
@supercat:有趣!处理IDisposable的另一个原因。 - Adam Robinson
顺便提一下,虽然前面提到的“集合”对象存在一些令人恼火的怪癖,限制了它在除了历史兼容性以外的其他用途上的实用性,但它也有一些独特的优点。其中最主要的是,它是唯一一个类似于字典的集合,可以轻松地删除所有符合某个条件的项目,而不必建立一个单独的要删除的项目列表。 - supercat
显示剩余4条评论

9
所以,我的问题是,如果你有一个不包含非托管资源的类型,实现IDisposable接口是否值得?
当某人在对象上放置IDisposable接口时,这告诉我创建者打算在该方法中执行某些操作,或者将来可能打算执行。在这种情况下,我总是调用Dispose以确保清理。即使它现在什么也不做,但它未来可能会做一些事情,如果在第一次编写代码时没有调用Dispose,那么可能会导致内存泄漏。
事实上,这是一个判断性的问题。你不想过度实现它,因为这样的话为什么还要使用垃圾回收器呢?为什么不手动处理每个对象的释放呢?如果存在需要释放非托管资源的可能性,那么这可能不是一个坏主意。这取决于具体情况,如果只有你团队中的人使用你的对象,你随时可以与他们联系并说:“嘿,现在需要使用非托管资源。我们必须检查代码并确保我们已经清理了。”如果你将其发布给其他组织使用,则情况就不同了。没有简单的方法告诉所有可能已经实现该对象的人,“嘿,你们需要确保这个对象现在被释放了。”让我告诉你,很少有什么比升级第三方程序集更让人生气的事情,结果发现他们是那些改变了代码并使你的应用程序出现内存泄漏问题的人。
我的同事回复说,在底层,ADO.NET连接是一种托管资源。我对他的回复是,最终所有东西都是非托管资源。
他是对的,它现在是一个托管资源。他们会改变它吗?谁知道呢,但调用它不会有任何损失。我不试图猜测ADO.NET团队做了什么,所以如果他们将其放入其中并且没有任何作用,那也没关系。我仍然会调用它,因为一行代码不会影响我的生产力。
你还会遇到另一种情况。假设你从一个方法中返回了一个ADO.NET连接。你不知道该ADO连接是基对象还是派生类型。你不知道是否已经需要IDisposable实现。无论如何,我总是调用它,因为当生产服务器每4小时崩溃时,跟踪内存泄漏很麻烦。

许多类型实现 IDisposable 不是因为它们期望将来的版本可能这样做,而是因为它们或基类型可能被用作工厂方法的返回类型,这些方法可能返回需要清理的派生类型。例如,所有实现 IEnumerator<T> 的类型都实现了 IDisposable,即使它们的绝大多数 Dispose 方法什么也不做。 - supercat

6
虽然已经有很好的答案,但我想更明确地说明一些内容。
实现IDisposable有三种情况:
1. 直接使用非托管资源。这通常涉及从P/Invoke调用中检索一个IntPtr或其他形式的句柄,该句柄必须通过不同的P/Invoke调用释放。 2. 使用其他IDisposable对象并需要负责其处置。 3. 您还有其他需要或用途,包括“using”块的便利性。
虽然我可能有点偏见,但您应该真正阅读(并向同事展示) 关于的StackOverflow Wiki

2
我建议更新Wiki,将生命周期管理作为实现IDisposable的原因之一。例如,IObservable<T>.Subscribe返回一个IDisposable,即使它不是用于封装非托管资源或在using块中使用。 - Gabe
1
@Gabe:这是一个维基百科,所以请随意编辑。我之前没有使用过IObservable<T>,如果您能添加一些内容可能会更好。 - Adam Robinson
1
@AdamRobinson:关于“总是”调用IDisposable应该有一些澄清。重要的是,在最后一个对IDisposable的引用被销毁之前(因为在此之后无法调用),必须调用Dispose。另一方面,许多对象通常持有对IDisposable的引用;通常只有一个对象应该调用Dispose - supercat
@supercat:是的,你说得对。对于任何给定的IDisposable,都应该有一个“所有者”,负责确保适当地调用Dispose - Adam Robinson

5
不,它不仅仅适用于非托管资源。
它被建议作为一个基本的清理机制内置于框架中,使您有可能清理任何资源,但它最适合的自然是非托管资源管理。

实现(和调用)Dispose对于大多数持有资源的类至关重要。托管/非托管大多不相关。 - H H
@HenkHolterman:对于我来说,似乎我的意思很明确:“它不仅用于非托管资源管理。”是这样吗? - Tigran
1
是的,抱歉。我忽略了那里的一个__不__。 - H H

5
请注意,非托管资源可能包括标准CLR对象,例如保存在某些静态字段中,在完全没有非托管导入的安全模式下运行。
无法简单地判断实现IDisposable的给定类是否需要进行清理。我的经验法则是始终对我不太了解的对象调用Dispose,例如某些第三方库。

值得注意的是,即使一个对象有一个 DisposeRequired 属性,找出是否需要调用 Dispose 的时间(稍微)超过了无条件调用 Dispose 的时间 [因为它必须对属性进行虚拟调用,然后根据结果进行分支,而不是简单地进行虚拟调用]。只有在确定何时调用 Dispose 比实际调用 Dispose 更繁琐时(例如,如果需要对需要清理的对象进行“用户计数”,但不需要对不需要清理的对象进行计数),DisposeRequired 才会有帮助。 - supercat

5

Dispose 应该用于任何具有 有限生命周期 的资源。对于任何非托管资源,都应该使用 finalizer。任何非托管资源都应该有一个有限的生命周期,但是还有很多托管资源(如锁)也具有有限的生命周期。


3

一切最终都是未受管理的资源。

不是真的。除了CLR对象使用的内存,这些内存只由框架进行管理(分配和释放)。

实现IDisposable并在一个不持有任何未受管理资源的对象上调用Dispose(直接或间接通过依赖对象),是毫无意义的。这并不能使释放该对象变得确定性,因为你不能直接自己释放对象的CLR内存,因为它总是只有GC才能做到。对象是引用类型,因为值类型,在方法级别直接使用时,是通过堆栈操作进行分配/释放的。

现在,每个人都声称自己的答案是正确的。让我来证明我的观点。根据文档:

Object.Finalize方法允许对象在被垃圾回收器回收之前尝试释放资源和执行其他清理操作。

换句话说,对象的CLR内存在调用Object.Finalize()后立即释放。[注意:如果需要,可以明确跳过此调用]
这是一个没有非托管资源的可处理类:
internal class Class1 : IDisposable
{
    public Class1()
    {
        Console.WriteLine("Construct");
    }

    public void Dispose()
    {
        Console.WriteLine("Dispose");
    }

    ~Class1()
    {
        Console.WriteLine("Destruct");
    }
}

请注意,析构函数会隐式调用继承链中的每个Finalize,直到Object.Finalize()
这是控制台应用程序的Main方法:
static void Main(string[] args)
{
    for (int i = 0; i < 10; i++)
    {
        Class1 obj = new Class1();
        obj.Dispose();
    }

    Console.ReadKey();
}

如果调用Dispose是一种以确定的方式释放托管对象的方法,那么每个“Dispose”都会立即跟随一个“Destruct”,对吗?亲自试试看会发生什么。从命令行窗口运行此应用程序最有趣。
注意:有一种方法可以强制GC收集当前应用程序域中挂起终结的所有对象,但无法针对单个特定对象。尽管您不需要调用Dispose来使对象进入终结队列,但强烈建议不要强制进行垃圾回收,因为这很可能会损害整个应用程序的性能。
编辑
有一个例外-状态管理。如果您的对象恰好管理外部状态,则Dispose可以处理状态更改。即使状态不是非托管对象,由于IDisposable的特殊处理,使用它也非常方便。例如,安全上下文或模拟上下文。
using (WindowsImpersonationContext context = SomeUserIdentity.Impersonate()))
{
    // do something as SomeUser
}

// back to your user

这不是最好的例子,因为WindowsImpersonationContext在内部使用系统句柄,但你可以理解意思。
总之,实现IDisposable时,你需要在Dispose方法中有(或计划有)一些有意义的事情要做。否则,这只是浪费时间。 IDisposable不会改变GC管理对象的方式。

1
你在这里是不正确的。该对象可能不会被“物理”垃圾回收,但在已知时间逻辑上将释放一些重要内容。这个“东西”可能不仅是一些未受管理的资源,还包括队列中的插槽、为某些计算分配线程池线程、使用其他程序的许可证等——即一个_逻辑_资源。对象在内存中的存在只是一个纯粹的细节,如果可处理对象以正确的方式实现,则不应考虑它。 - Vlad
@Vlad,你错过了我的回答的一些部分。你提到的所有“something”最终都是非托管资源。如果你的对象处理这些资源(直接或间接地通过依赖对象),它就变成了一个非托管对象,并且应该实现Dispose。换句话说,如果任何依赖对象实现了Dispose,你的对象也应该实现。如果没有表述清楚,我很抱歉。 - Maciej
1
好的,我并不质疑你整个答案,只是关于“在一个不持有任何非托管资源的对象上调用Dispose(...)是没有意义的”这部分。我试图找一些资源本质上是托管的例子,尽管需要以确定的方式释放。例如,修改集合的权利 明显不是非托管资源,但使用 IDisposableRAII-way 的方式获取此权限是一件好事:using (ObtainModifyRight(collection)) { /* modify it */ } - Vlad
1
使用Dispose来管理状态有点像劫持Dispose模式,因为它是用于非托管资源的。但我同意这种情况有时会发生,并且由于using关键字的存在而变得更加方便。更新答案。 - Maciej
如果将修改集合的权利赋予一个对象会影响其他对象修改该集合的能力,直到接收该权利的对象表示不再需要它,那么修改集合的权利就是一种非受控资源。毕竟,一个对象从操作系统请求一块非受控内存,这到底意味着什么呢?除了(1)操作系统给予对象使用该内存的权利,以及(2)在第一个对象告诉操作系统不再需要该内存之前,其他人都无法使用它。 - supercat
@Maciej - 关于“无意义”的评论,也许你应该阅读我在其他帖子中链接到我的答案的其他解释。至于“证明”你的答案,文档是一回事,但除非你已经使用WinDbg调试过OOM异常内存转储,并且了解GC的内部工作原理,否则你应该更多地依赖经验而不是文档。 - Dave Black

3
如果您汇总了IDisposable,则应实现该接口,以便这些成员及时清理。否则,在您引用的ADO.Net连接示例中,myConn.Dispose()如何被调用呢?但在这种情况下,我认为不正确地说所有内容都是无管理资源。我也不同意您的同事的观点。

3

你说得对。管理的数据库连接、文件、注册表键、套接字等都持有未托管对象。这就是为什么它们实现了IDisposable接口。如果你的类型拥有可释放的对象,你应该实现IDisposable并在你的Dispose方法中释放它们。否则它们可能会一直存活,直到垃圾回收导致锁定文件和其他意外行为。


嗯,听起来你似乎同意原帖的同事而非原帖本人。 - BlueRaja - Danny Pflughoeft
我认为我的同事的观点是,如果你的类型有一个托管的ADO连接,那么它不必实现IDisposable,因为资源是托管的。但我的观点是,你必须这样做,因为在任何IDisposable对象内部都有一个非托管的资源。当你聚合IDisposable对象时,你也必须实现IDisposable - Martin Liversage
在这种情况下,有未经管理的资源需要清理。同事认为如果没有未经管理的资源,则不需要使用IDisposable - OP似乎认为即使没有未经管理的资源需要清理,实现IDisposable也有价值。 - BlueRaja - Danny Pflughoeft
@BlueRaja-DannyPflughoeft:我想问题有点含糊不清。我的问题主要是回应“我的回复是,最终一切都是未经管理的资源。”我支持这种说法。 - Martin Liversage

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