67

我最近和一位同事讨论了Dispose实现IDisposable.

我认为实现IDisposable应该尽快清理的类型是有价值的,即使没有要清理的非托管资源

我的同事想法不同;IDisposable如果您没有任何非托管资源,则无需实施,因为您的类型最终将被垃圾收集。

我的论点是,如果您有一个想要尽快关闭的 ADO.NET 连接,那么实施IDisposableusing new MyThingWithAConnection()有意义。我的同事回答说,在幕后,ADO.NET 连接是非托管资源。我对他的答复是,一切最终都是非托管资源

我知道推荐的一次性模式,如果被调用,则释放托管和非托管资源,Dispose如果通过终结器/析构函数调用,则仅释放非托管资源(并在不久前发表了关于如何提醒消费者不当使用 IDisposable 类型的博客)

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

4

15 回答 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个副本)等。这里要释放的对象不是非托管资源,而是做/使用某事的权利,这纯粹是程序逻辑的内部构造。


小补充:这里是 [ab] 使用的简洁示例列表:http IDisposable: //www.introtorx.com/Content/v1.0.10621.0/03_LifetimeManagement.html#IDisposable

于 2012-04-25T13:14:55.927 回答
17

我认为IDisposable责任的角度考虑是最有帮助的。如果一个对象IDisposable知道在不再需要它的时间和宇宙终结之间需要完成的事情(最好尽快),并且它是唯一具有信息和推动力的对象,则它应该实现做。例如,打开文件的对象有责任查看文件是否关闭。如果对象只是在不关闭文件的情况下消失,则文件可能不会在任何合理的时间范围内关闭。

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

请注意,有时可以使用终结器进行清理,因为无论出于何种原因,一个对象在没有Dispose被首先调用的情况下就被放弃了。有时这很好用;有时它工作得非常糟糕。例如,即使 Microsoft.VisualBasic.Collection使用终结器从“修改”事件中分离枚举器,尝试在没有干预Dispose或垃圾收集的情况下枚举此类对象数千次也会导致它变得非常慢 - 比性能慢许多数量级如果使用Dispose得当,就会出现这种情况。

于 2012-04-25T13:45:42.767 回答
9

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

当有人在对象上放置 IDisposable 接口时,这告诉我创建者打算在该方法中做某事,或者将来他们可能打算这样做。为了确定,我总是在这种情况下调用 dispose 。即使它现在不做任何事情,它也可能在将来做任何事情,并且由于他们更新了一个对象而导致内存泄漏很糟糕,而且您在第一次编写代码时没有调用 Dispose。

事实上,这是一个判断电话。你不想过度实现它,因为在那个时候为什么还要麻烦有一个垃圾收集器。为什么不手动处理每个对象。如果您有可能需要处置非托管资源,那么这可能不是一个坏主意。这一切都取决于,如果唯一使用您的对象的人是您团队中的人,您可以随时跟进他们并说,“嘿,现在需要使用非托管资源。我们必须检查代码并确保我们已经收拾好了。” 如果您将其发布以供其他组织使用,那就不同了。没有简单的方法可以告诉可能已经实现该对象的每个人,“嘿,您需要确保它现在已被处置。”

我的同事回答说,在幕后,ADO.NET 连接是一种托管资源。我对他的答复是,一切最终都是非托管资源。

他是对的,它现在是一个托管资源。他们会改变它吗?谁知道呢,但打电话给它也无妨。我不会尝试猜测 ADO.NET 团队做了什么,所以如果他们把它放进去但它什么也没做,那很好。我还是会这样称呼它,因为一行代码不会影响我的工作效率。

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

于 2012-04-25T13:18:32.627 回答
6

虽然已经有了很好的答案,但我只是想明确一点。

实现的三种情况IDisposable

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

虽然我可能有点偏见,但您应该真正阅读(并向您的同事展示)StackOverflow Wiki onIDisposable .

于 2012-04-25T18:21:49.100 回答
5

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

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

于 2012-04-25T13:16:12.993 回答
5

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

没有简单的方法来判断一个给定的类实现是否IDiposable真的需要清理一些东西。我的经验法则是总是调用Dispose我不太了解的对象,比如一些 3rd 方库。

于 2012-04-25T13:18:21.300 回答
5

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

于 2012-04-25T13:20:21.797 回答
3

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

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

于 2012-04-25T13:17:03.187 回答
3

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

于 2012-04-25T13:17:51.113 回答
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 管理对象的方式。

于 2012-05-01T20:55:07.967 回答
1

如果你的类型引用了非托管资源或者它持有对实现 IDisposable 的对象的引用,则你的类型应该实现 IDisposable。

于 2012-04-25T14:26:44.033 回答
1

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

A 用于在退出时处理 C。B 曾经使用 C 来保存异常。

我的班级必须实现 IDisposable 和 decrtuctor 以确保以正确的顺序处理事物。是的,GC 可以清理我的物品,但我的经验是除非我管理好班级的清理工作,否则会出现竞争状况。

于 2012-05-18T15:31:12.643 回答
1

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

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

至于 IDisposable 实现的最佳实践,请参考我的博文:

您如何正确实现 IDisposable 模式?

于 2012-08-31T14:16:28.750 回答
1

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

如果一个对象使用非托管资源,它应该实现IDisposable. 拥有一次性对象的对象应实现IDisposable以确保释放底层非托管资源。如果遵循规则/约定,因此可以得出结论,不释放托管一次性对象等于不释放非托管资源是合乎逻辑的。

于 2015-08-01T03:10:26.037 回答
1

根本不需要资源(托管或非托管)。通常,IDisposable这只是消除 conbersome的一种方便方法try {..} finally {..},只需比较:

  Cursor savedCursor = Cursor.Current;

  try {
    Cursor.Current = Cursors.WaitCursor;

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

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

哪里适合:WaitCursor_IDisposableusing

  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();
  }
于 2015-10-26T16:08:30.430 回答