“释放”是否应仅用于包含非托管资源的类型

本文关键字:资源 类型 包含非 用于 释放 是否 | 更新日期: 2023-09-27 18:32:16

我最近和一位同事讨论了实现IDisposableDispose和类型的价值。

我认为对于应该尽快清理的类型实施IDisposable是有价值的,即使没有要清理的非托管资源也是如此。

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

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

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

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

“释放”是否应仅用于包含非托管资源的类型

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]使用IDisposable的简洁示例:http://www.introtorx.com/Content/v1.0.10621.0/03_LifetimeManagement.html#IDisposable。

我认为从

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

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

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

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

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

事实上,这是一个判断电话。 你不想过度实现它,因为在这一点上,为什么要费心使用垃圾收集器。 为什么不直接手动释放每个对象。 如果可能需要释放非托管资源,则可能不是一个坏主意。 这完全取决于,如果唯一使用您的对象的人是团队中的人员,您可以随时跟进他们并说:"嘿,现在需要使用非托管资源。 我们必须检查代码并确保我们已经整理好了。 如果要将其发布给其他组织使用,则有所不同。 没有简单的方法可以告诉每个可能已经实现该对象的人,"嘿,你需要确保它现在已经被释放了。 让我告诉你,没有什么比升级第三方程序集更让人们抓狂的事情了,发现他们是更改代码并使你的应用程序出现内存问题的人。

我的同事回答说,在幕后,ADO.NET 联系是一个 托管资源。我对他的回答的回答是,一切最终 是非托管资源。

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

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

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

实现IDisposable有三种情况:

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

虽然我可能有点偏见,但你真的应该阅读(并向你的同事展示)IDisposable上的 StackOverflow Wiki。

Dispose应该用于任何生存期有限的资源。终结器应用于任何非托管资源。任何非托管资源都应具有有限的生存期,但有大量托管资源(如锁)也具有有限的生存期。

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

没有简单的方法来判断实现IDiposable的给定类是否真的需要清理某些东西。我的经验法则是始终对我不太熟悉的对象调用Dispose,例如某些第三方库。

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

建议将其与框架调用的基本清理内置机制类似,使您能够清理所需的任何资源,但它最适合的自然是非托管资源管理。

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

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

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

一切最终都是非托管资源。

不对。除 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是一种以确定性方式释放托管对象的方法,那么每个"释放"之后都会立即跟一个"破坏",对吗?亲眼看看会发生什么。从命令行窗口运行此应用程序最有趣。

注意:有一种方法可以强制GC收集当前应用程序域中等待完成的所有对象,但不会收集单个特定对象的所有对象。不过,您无需调用 Dispose 即可在终结队列中拥有对象。强烈建议不要强制收集,因为它可能会损害整体应用程序性能。

编辑

有一个例外 - 状态管理。 Dispose可以处理状态更改,如果对象碰巧管理外部状态。即使状态不是非托管对象,由于IDisposable具有特殊处理,因此像使用状态一样使用它也非常方便。示例是安全上下文或模拟上下文。

using (WindowsImpersonationContext context = SomeUserIdentity.Impersonate()))
{
    // do something as SomeUser
}
// back to your user

这不是最好的例子,因为WindowsImpersonationContext内部使用系统句柄,但你会得到图片。

底线是,在实现IDisposable时,您需要(或计划)在Dispose方法中做一些有意义的事情。否则只是浪费时间。 IDisposable不会更改 GC 管理对象的方式。

如果类型引用非托管资源或包含对实现 IDisposable 的对象的引用,则类型应实现 IDisposable。

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

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

我的类必须实现 IDisposable 和描述器,以确保以正确的顺序处理东西。是的,GC 可以清理我的物品,但我的经验是,除非我设法清理我的班级,否则存在竞争条件。

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

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

  • 依赖 .NET 自动垃圾回收器是否不好?
  • 如果我不对笔对象调用 Dispose 会发生什么情况?
  • 处置,什么时候叫?

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

如何正确实现 IDisposable 模式?

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

如果对象使用非托管资源,则应实现 IDisposable 。拥有一次性对象的对象应实现IDisposable,以确保释放基础非托管资源。 如果遵循规则/约定,则合乎逻辑的结论是,不释放托管的一次性对象等于不释放非托管资源。

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

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

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

其中WaitCursor IDisposable适合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();
  }