“处置”是否仅应用于包含非托管资源的类型?


67

我最近正在与一位同事讨论Dispose实现的价值和类型IDisposable

我认为IDisposable即使没有任何非托管资源需要清理,实现应尽快清理的类型也很有价值。

我的同事有不同的看法。执行IDisposable,如果你没有任何非托管资源为你的类型,最终会被垃圾收集是没有必要的。

我的观点是,如果您希望尽快关闭ADO.NET连接,则可以实施IDisposable并且using new MyThingWithAConnection()有意义。我的同事回答说,在幕后,ADO.NET连接是不受管理的资源。我对他的答复是,所有事情最终都是不受管理的资源

我知道的建议一次性模式,你免费托管和非托管资源,如果Dispose被调用,但如果通过终结/析构函数称为唯一的免费托管资源(和博客前一阵子关于如何提醒消费者不当使用您的IDisposable类型的

因此,我的问题是,如果您的类型不包含非托管资源,是否值得实现IDisposable


2
如您正确指出的那样,ADO连接是非托管资源。
康拉德·鲁道夫2012年

1
@KonradRudolph-否。连接称为托管资源。它包含(拥有)一个非托管资源,尽管可能是通过SafeHandle间接获得的。
Henk Holterman

@Henk这就是我的意思–我应该更仔细地措辞,但问题是它已经以正确的方式表达了。
康拉德·鲁道夫

2
IDisposable不受管理的资源之外,我唯一需要的其他时间是何时需要确保事件正确地取消订阅,以便可以对类进行垃圾回收。但是,这是真正的语言的失败:事件真的真的真的需要是弱引用,但他们没有。
BlueRaja-Danny Pflughoeft 2012年

Answers:


36

的有效使用有不同IDisposable。一个简单的示例是保存一个打开的文件,一旦您不再需要它,则需要在某个时刻关闭它。当然,您可以提供一个method Close,但是将其包含在其中Dispose并使用类似patternusing (var f = new MyFile(path)) { /*process it*/ }会更安全。

一个比较流行的示例是拥有一些其他IDisposable资源,这通常意味着您还需要提供自己Dispose的资源才能进行处理。

通常,只要您想确定性地销毁任何内容,就需要实施IDisposable

我和您的意见之间的区别是,我IDisposable需要在确定性销毁/释放某些资源时立即实施,而不必尽快实施。在这种情况下,依靠垃圾回收不是一种选择(与您的同事的主张相反),因为它发生在不可预测的时刻,实际上可能根本没有发生!

任何资源都是由后台管理的事实实际上并不意味着任何事情:开发人员应该从“何时以及如何正确处置此对象”的角度思考,而不是“在后台如何工作”。底层实现可能会随时间而改变。

实际上,C#和C ++之间的主要区别之一是没有默认确定性销毁。该IDisposable来缩小差距:您可以订购确定性破坏(虽然你不能保证客户端调用它;在C ++中以同样的方式,你不能确保客户端调用delete的对象)。


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

如果要确定性地释放资源,则意味着客户端代码应该可以说“现在,我要释放该资源”。实际上,这可能不是最早的释放资源的时间:持有资源的对象可能已经从资源中获得了所需的一切,因此可能已经释放了资源。另一方面,对象可能会选择保留(通常是非托管的)资源,即使在对象Dispose通过之后也是如此,仅在终结器中对其进行清理(如果将资源保留的时间过长不会造成任何问题)。

因此,严格来说,不必尽快释放资源Dispose:对象可以在意识到自身不再需要资源后立即释放资源。Dispose但是,这可以作为有用的提示,即不再需要该对象本身,因此在适当的时候,也许可以在那时释放资源。


另外一个必要的补充是:不仅需要确定性释放的非托管资源!这似乎是这个问题的答案之间意见分歧的关键点之一。一个人可能具有纯粹的想象力构造,可能需要确定性地释放它。

例如:访问某些共享结构的权限(例如RW-lock),巨大的内存块(例如您正在手动管理程序的某些内存),使用其他程序的许可证(例如您不允许运行超过X同时这里是一些程序的副本)等被释放的对象不是一个非托管资源,但正确的做/使用的东西,这是一个纯粹的内部构造程序逻辑。


小添加:这是[ab]使用的简洁示例的一小部分IDisposablehttp : //www.introtorx.com/Content/v1.0.10621.0/03_LifetimeManagement.html#IDisposable


2
除了文件和数据库连接之类的非托管资源之外,什么时候需要确定性销毁? (也是轻微的抱怨-对象本身没有确定性地被破坏,仅是它使用的资源...并且仅在那些资源
未被

@ BlueRaja-DannyPflughoeft:请参阅我的回答,但直接回答:1)当您通过实现的其他对象间接使用这些资源IDisposable时,或者无论出于何种原因使用了IDisposable成语(以及该using块的便利性)时。
亚当·罗宾逊

@亚当:情况1就是没有管理的资源需要清理的情况。情况2与我的问题正交,无论如何都不是真正的例子。
BlueRaja-Danny Pflughoeft 2012年

@ BlueRaja-DannyPflughoeft:是的,尽管模式是不同的,具体取决于您是直接使用非托管资源还是通过抽象来使用。至于第二种,任何具有入口和出口点的模式都适用。尽管lock已经存在,但使用可以很容易地重现相同的行为using。例如,在您临时更改状态并希望确保将其更改回...(例如更改鼠标光标)时,它也很有用。您可以在构造函数中进行更改,然后将其重置为Dispose
亚当·罗宾逊

2
@ BlueRaja-DannyPflughoeft:对锁定对象的独占访问一种资源。如果拥有独占访问权的对象被放弃而没有通知被保护对象不再需要排他性,则没有人可以使用该对象。对于事件,如果无数个短寿命对象订阅了一个长寿命对象的事件,但此后不久就被放弃了,则此类对象将造成无穷大的内存泄漏,因为它们将在生命周期中不符合收集条件。寿命更长的对象。
2015年

17

我认为IDisposable责任方面考虑是最有帮助的。如果对象IDisposable知道在不再需要的时间到宇宙结束之间(最好尽快)之间需要做的事情,并且它是唯一具有信息和动力的对象,则应该实现做吧。例如,打开文件的对象将负责查看文件已关闭。如果对象只是在不关闭文件的情况下就消失了,则文件可能不会在任何合理的时间内关闭。

需要特别注意的是,即使仅与100%受管对象进行交互的对象也可以执行需要清理(并应使用IDisposable)的操作。例如,IEnumerator附加到集合“已修改”事件的,将在不再需要时将其自身分离。否则,除非枚举器使用一些复杂的欺骗手段,否则只要收集器在范围内,就永远不会对其进行垃圾收集。如果对集合进行一百万次枚举,则一百万枚枚举数将附加到其事件处理程序。

请注意,有时出于某种原因而Dispose无需先调用就放弃对象的情况下,可以使用终结器进行清理。有时,这很好用;someitmes它的工作非常糟糕。例如,即使 Microsoft.VisualBasic.Collection使用终结器将枚举器与“已修改”事件分离,尝试在没有干预Dispose或垃圾回收的情况下枚举此类对象数千次也会导致其变得非常慢-比性能慢许多数量级如果Dispose正确使用将导致这种情况。


1
谢谢,我希望我将IEnumerator示例视为反驳!
史蒂夫·邓恩

2
@SteveDunn:谢谢。似乎普遍认为,短语“非托管资源”中的“非托管”与“非托管代码”有关。现实是这两个概念在很大程度上是正交的。终结器可能会有点混淆“清理责任”问题,因为如果没有终结者,那么“宇宙终结之前”语言可能有些字面意义。如果没有终结器的对象仅持有一个句柄的副本,该副本将授予对某些内容的独占访问权,并且在不释放该句柄的情况下将其丢弃,则该句柄实际上不会被释放。
supercat 2012年

1
@SteveDunn:当然,是否释放手柄的问题可能在宇宙终结之前就变得很无聊了,但是关键是一旦所有的副本都消失了,就不会释放手柄了。因此,具有该副本的最后一个实体必须确保在该句柄副本仍然存在的情况下释放它。顺便提一下,完全在托管代码中的“非托管资源”的另一个很好的例子:锁。
supercat 2012年

even objects which only interact with 100% unmanaged objects 那不应该读取100%MANAGED对象吗?否则,很好的答案。如果您的实现拥有一个IDisposable实例,那么您也应该实现该实例以对其进行清理,并且IDisposable是传达该实例的唯一方法。
安迪

@安迪:固定。我的主要观点是,很多人似乎认为“非托管资源”一词的意思是“由本机代码处理的资源”,而事实并非如此。尽管由本机代码管理的资源几乎总是非托管资源,但它们并不是唯一重要的类型。实际上,我真的不喜欢“托管资源”和“非托管资源”这两个术语,因为它们之间的含义几乎没有一致性。不能在自身外部进行任何操作的东西是“托管资源”,还是该术语仅指代带有终结器的对象?
supercat 2012年

9

所以,我的问题是,如果您的类型不包含非托管资源,是否值得实现IDisposable?

当有人在对象上放置一个IDisposable接口时,这告诉我创建者打算以此方式做某事,或者将来可能打算这样做。为了确保这种情况,我总是在这种情况下称呼处理。即使它现在不执行任何操作,也可能会在将来发生,并且由于它们更新了一个对象而使它很糟,从而导致内存泄漏,并且您在第一次编写代码时没有调用Dispose。

实际上,这是一个判断电话。您不想过度实现它,因为在那一点上,为什么要烦恼根本没有垃圾收集器。为什么不手动处理每个对象呢?如果有可能需要处置非托管资源,那么它可能不是一个坏主意。这取决于,如果使用对象的唯一人员是团队中的人员,那么您以后总是可以跟进他们,并说:“嘿,这需要立即使用非托管资源。我们必须仔细阅读代码,并确保我们整理了。” 如果要发布此内容供其他组织使用,则有所不同。没有简单的方法告诉可能已经实现该对象的每个人,“嘿,您需要确保现在就处置了它。”

我的同事回答说,在幕后,ADO.NET连接是一种托管资源。我对他的答复是,所有内容最终都是不受管理的资源。

他是对的,现在是托管资源。他们会改变吗?谁知道,但称它为无害。我不会尝试猜测ADO.NET团队的工作,因此,如果他们将其放入却什么也没做,那就很好。我仍然称呼它,因为一行代码不会影响我的工作效率。

您还会遇到另一种情况。假设您从方法返回ADO.NET连接。您不知道ADO连接是基础对象还是事实的派生类型。您不知道该IDisposable实现是否突然变得必要。无论如何,我总是称呼它,因为在每4个小时崩溃时,跟踪生产服务器上的内存泄漏很糟糕。


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

6

尽管已经有了很好的答案,但我只是想明确一些。

有以下三种情况可以实施IDisposable

  1. 您正在直接使用非托管资源。这通常涉及IntPrt从必须由其他P / Invoke调用释放的P / Invoke调用中检索一个或其他形式的句柄。
  2. 您正在使用其他 IDisposable对象,需要对它们的处理负责
  3. 您还有其他需求或用途,包括该using块的便利性。

尽管我可能有点偏见,但是您应该真正阅读(并向您的同事展示)StackOverflow Wiki上的内容IDisposable


2
我建议更新Wiki以将生命周期管理作为实施的理由IDisposable。例如,即使不打算封装非托管资源或在块中使用它,也要IObservable<T>.Subscribe返回a 。IDisposableusing
加布

1
@Gabe:这是一个Wiki,请随时进行编辑。我以前没用过IObservable<T>,所以如果您可以添加一些内容可能会更好。
亚当·罗宾逊

1
@AdamRobinson:应该对“ always”电话进行一些澄清IDisposable。请务必DisposeIDisposable销毁最后一个对an​​的引用之前调用它(因为之后不能调用),这一点很重要。另一方面,对于许多对象来说,持有对IDisposable;的引用是很常见的。通常,恰好有人应致电Dispose
2012年

@supercat:是的,您是对的。任何给定IDisposable负责确保Dispose适当调用的给定都应该有一个“所有者” 。
亚当·罗宾逊

5

Dispose应该用于寿命有限的任何资源。终结器应用于任何非托管资源。任何非托管资源的生存期都应该有限,但是有很多托管资源(如锁)的生存期也很有限。


5

请注意,非托管资源很可能包括标准的CLR对象,例如保存在某些静态字段中的所有对象,都以安全模式运行,根本没有非托管导入。

没有简单的方法可以判断给定的实现类是否IDiposable确实需要清除某些内容。我的经验法则是总是调用Dispose我不太了解的对象,例如某些第三方库。


值得注意的是,即使对象具有DisposeRequired属性,发现Dispose必要Dispose条件所需的时间也会(略)超过无条件调用所需的时间(因为它必须先虚拟调用该属性然后分支结果,而不是简单地进行虚拟呼叫]。唯一DisposeRequired有用的时间是,确定何时调用Dispose比实际调用更为繁重(例如,如果需要清除的对象需要“用户计数”,而不需要清除的对象则不需要)。
2013年

5

不,它不仅 适用于非托管资源。

建议像框架调用的基本内置清理机制一样,它使您可以清理所需的任何资源,但最适合的自然是非托管资源管理。


对于大多数资源持有类,实现(和调用)Dispose至关重要。托管/非托管大多数无关紧要。
Henk Holterman

@HenkHolterman:在看来,我的确切意思是:“它不仅用于不受管理的资源管理。” 是不是
Tigran '04

1
是的对不起 我忽略了一个不在那里。
Henk Holterman

3

如果聚合IDisposables,则应实现该接口,以便及时清理这些成员。myConn.Dispose()您引用的ADO.Net连接示例中还如何调用它?

在这种情况下,我认为一切都是非托管资源是不正确的。我也不同意你的同事。


3

你是对的。托管数据库连接,文件,注册表项,套接字等都保留在非托管对象上。这就是他们实施的原因IDisposable。如果您的类型拥有一次性对象,则应IDisposableDispose方法中实现并处置它们。否则,它们可能一直存在,直到收集到垃圾导致锁定的文件和其他意外行为为止。


嗯,听起来您是在同意OP的同事,而不是OP。
BlueRaja-Danny Pflughoeft 2012年

好吧,我假设同事认为,如果您的类型具有托管的ADO连接,则不必实施,IDisposable因为资源是托管的。我要说的是,您必须这样做,因为在任何IDisposable对象的深处都有不可管理的资源。聚合IDisposable对象时,还必须实现IDisposable
Martin Liversage 2012年

在这种情况下,有一些非托管资源需要清理。同事争辩说,如果没有不受管理的资源,则不需要IDisposable-OP似乎认为IDisposable即使没有要清除的不受管理的资源,实现[..]还是有价值的”。
BlueRaja-Danny Pflughoeft 2012年

@ BlueRaja-DannyPflughoeft:我想这个问题有点模棱两可。我的问题主要是对我的答复。我对他的答复是,所有事情最终都是不受管理的资源。我支持该主张。
Martin Liversage 2012年

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
你不在这里 该对象可能不是“物理上”的垃圾回收,但在逻辑上它将在已知时间释放重要的东西。这种“东西”不仅可能是一些非托管资源,而且可能是队列中的插槽,用于某些计算的线程池线程分配,使用某些其他程序的许可等-即逻辑资源。内存中对象的存在只是一个纯粹的细节,如果以正确的方式实现一次性对象,则不应将其考虑在内。
Vlad12年

@Vlad您错过了我的部分答案。您提到的所有这些“东西”最终都是不受管理的资源。如果您的对象(直接或间接通过相关对象)处理这些对象,则该对象将成为不受管理的对象,并应实现Dispose。换句话说,如果有任何依赖对象实现,那么Dispose您的对象也应该实现。很抱歉,如果没有明确指出。
Maciej

1
好吧,我不是要对您的答案提出全部疑问,而只是“对不保留任何非托管资源(...)的对象调用Dispose是毫无意义的”部分。我试图得到一些示例,其中需要对资源进行实质上的管理,尽管需要以确定性的方式进行释放。例如,修改收取权利显然不是非托管资源,但在获得这种权利RAII路IDisposable是一件好事:using (ObtainModifyRight(collection)) { /* modify it */ }
弗拉德

1
使用Dispose管理状态有点劫持Dispose模式,因为它是针对非托管资源的。但是我同意这种情况有时会发生并且很方便,因为有using关键字。更新答案。
Maciej

如果在一个对象上授予该权限,将损害其他对象修改该集合的能力,直到收到该权限的对象表明不再需要该权限,则该修改权限是不受管理的资源。毕竟,对象向操作系统请求非托管内存块的真正含义是什么,除了(1)操作系统授予对象使用该内存的权利,以及(2)其他人将无法使用该内存使用它直到第一个对象告诉操作系统它不再需要它了?
2012年

1

如果您的Type引用非托管资源或持有对实现IDisposable的对象的引用,则应实现IDisposable。


1

在我的一个项目中,我有一个带有托管线程的类,我们将其称为线程A,线程B,以及IDisposable对象,将其称为C。

用于退出时处置C的A。B曾经使用C保存异常。

我的班不得不实现IDisposable和一个描述符,以确保按正确的顺序处理事物。是的,GC可以清理我的物品,但是我的经验是,除非我管理好班级的清理工作,否则会有比赛条件。


1

简短的回答:绝对不会。如果您的类型具有托管或非托管成员,则应实现IDisposable。

现在详细说明:我已经回答了这个问题,并在StackOverflow上提供了有关内存管理和GC内部细节的更多详细信息。这里仅仅是少数:

至于实施IDisposable的最佳做法,请参阅我的博客文章:

您如何正确实现IDisposable模式?


1

完全没有必要的资源(托管或非托管)。通常,IDisposable这只是消除compersome的便捷方法try {..} finally {..},只需比较一下:

  Cursor savedCursor = Cursor.Current;

  try {
    Cursor.Current = Cursors.WaitCursor;

    SomeLongOperation();
  }
  finally {
    Cursor.Current = savedCursor;
  }

  using (new WaitCursor()) {
    SomeLongOperation();
  }

其中WaitCursorIDisposable为适合于using

  public sealed class WaitCursor: IDisposable {
    private Cursor m_Saved;

    public Boolean Disposed {
      get;
      private set;
    }

    public WaitCursor() {
      Cursor m_Saved = Cursor.Current;
      Cursor.Current = Cursors.WaitCursor;
    }

    public void Dispose() {
      if (!Disposed) {
        Disposed = true;
        Cursor.Current = m_Saved;
      }
    }
  }

您可以轻松组合以下类:

  using (new WaitCursor()) {
    using (new RegisterServerLongOperation("My Long DB Operation")) {
      SomeLongRdbmsOperation();  
    }

    SomeLongOperation();
  }

0

实现IDisposable如果对象拥有任何非托管对象任何托管对象一次性

如果对象使用非托管资源,则应实现IDisposable。拥有一次性对象的对象应实现IDisposable以确保释放基础的非托管资源。如果遵循规则/惯例,那么逻辑上得出的结论是,不处置托管的一次性对象等于不释放非托管的资源。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.