【问题标题】:Dealing with .NET IDisposable objects处理 .NET IDisposable 对象
【发布时间】:2010-09-20 06:39:10
【问题描述】:

我在 C# 中工作,我一直在使用 using 块来声明实现 IDisposable 的对象,这显然是你应该做的。但是,我看不到一个简单的方法来知道我何时滑倒。 Visual Studio 似乎没有以任何方式表明这一点(我只是错过了什么吗?)。我是不是应该在每次声明任何东西时检查帮助,并逐渐建立一个百科全书式的记忆,哪些对象是一次性的,哪些不是一次性的?似乎没有必要、痛苦且容易出错。

如何处理这个问题?

编辑:

查看相关问题侧边栏,我发现another question 清楚地表明Dispose() 无论如何都应该由对象的终结器调用。因此,即使您从未自己调用它,它最终也应该会发生,这意味着如果您不使用 using(我想这是我一直以来真正担心的问题),您将不会发生内存泄漏。唯一需要注意的是,垃圾收集器不知道对象作为非托管对象持有多少额外内存,因此它无法准确知道通过收集对象将释放多少内存。这将导致垃圾收集器的性能不如平时理想。

简而言之,如果我错过了using,这并不是世界末日。我只是希望某些东西至少会产生一个警告。

(题外话:为什么没有特殊的markdown链接到另一个问题?)

编辑:

好吧,好吧,别吵了。打电话给Dispose() 是超级骗子dramatic-chipmunk 级别重要,否则我们都会

现在。鉴于此,为什么这么容易——见鬼,为什么甚至允许——做错事?你必须不遗余力地做正确的事。像其他所有事情一样这样做会导致世界末日(显然)。封装这么多吧?

[大吃一惊,厌恶]

【问题讨论】:

  • 你不会在这个星球上找到一个有经验的 .net 程序员会同意你的看法。我并不是要夸张,但你真的应该选择一本好的 .Net 书。
  • @Atario - 你真的应该考虑改变接受的答案;坦率地说,Timwi 是错误的——或者充其量是过度简化事情,以至于两者(错误与简化)非常接近。
  • 您应该尽快调用 dispose,因为它可能会在下一次垃圾回收完成之前很久就释放资源。如果一个对象正在使用您的服务器或桌面上的资源,该资源将由 Dispose() 释放,您真的要等待垃圾收集器处理它吗?

标签: c# .net visual-studio idisposable using-statement


【解决方案1】:

FxCop 可能 帮助(尽管它没有发现我刚刚对其进行的测试);但是是的:您应该检查一下。 IDisposable 是系统中如此重要的一部分,您需要养成这种习惯。使用智能感知来寻找.D 是一个好的开始(虽然并不完美)。

不过,熟悉需要处理的类型并不需要很长时间;例如,通常涉及任何外部事物(连接、文件、数据库)。

ReSharper 也可以完成这项工作,它提供了“使用构造”选项。不过,它不会将其作为错误提供...

当然,如果你不确定 - 试试 using它:如果你偏执,编译器会嘲笑你:

using (int i = 5) {}

Error   1   'int': type used in a using statement must be implicitly convertible to 'System.IDisposable'    

【讨论】:

    【解决方案2】:

    与 Fxcop(与之相关)一样,VS 中的代码分析工具(如果您有更高版本之一)也会发现这些情况。

    【讨论】:

    • 其实我已经试过FxCop和VSTS分析了;在琐碎的测试中都没有发现错过的“使用”......
    【解决方案3】:

    始终尝试使用“使用”块。对于大多数对象,它并没有太大的区别,但是我遇到了一个最近的问题,我在一个类中实现了一个 ActiveX 控件,并且除非正确调用了 Dispose,否则没有优雅地清理。最重要的是,即使它似乎没有太大的不同,也要尝试正确地去做,因为一段时间后它会有所作为。

    【讨论】:

    • 这是解决 COM 互操作/ActiveX 互操作问题的经典解决方法。用using 整理这些对象当然是一个论据。这不一定是用using 整理每个 对象的论据
    • 我不知道我是否会将其称为“解决方法”;它更多的是清理可能崩溃或导致问题的非托管资源。至于“整理每个对象的论据” - 我的意见是您应该始终 .Dispose 实现 IDisposable 的对象,除非有很好的理由,否则“使用”块通常是最简单的机制。这不是真正的争论,只是我的看法。其他,例如Scott Dorman 的回答,在争论中做了更多的尝试。
    【解决方案4】:

    如果一个对象实现了IDisposable 接口,那么这是有原因的,你应该调用它,它不应该被视为可选的。最简单的方法是使用 using 块。

    Dispose() 不打算仅由对象的终结器调用,事实上,许多对象将实现 Dispose() 但没有终结器(这是完全有效的)。

    dispose 模式背后的整个想法是,您提供了一种确定性的方式来释放由对象(或其继承链中的任何对象)维护的 非托管 资源。如果没有正确调用Dispose(),您绝对会遇到内存泄漏(或任何数量的其他问题)。

    Dispose() 方法与析构函数没有任何关系。在 .NET 中最接近析构函数的是终结器。 using 语句不执行任何解除分配...实际上调用 Dispose() 不会在托管堆上执行任何解除分配;它只释放已分配的非托管资源。在 GC 运行并收集分配给该对象图的内存空间之前,不会真正释放托管资源。

    确定一个类是否实现IDisposable 的最佳方法是:

    • IntelliSense(如果它有 Dispose()Close() 方法)
    • MSDN
    • 反射器
    • 编译器(如果它没有实现IDisposable,则会出现编译器错误)
    • 常识(如果感觉你应该在完成后关闭/释放对象,那么你可能应该
    • 语义(如果有Open(),可能有对应的Close()应该被调用)
    • 编译器。尝试将它放在using 语句中。如果没有实现 IDisposable,编译器会报错。

    将 dispose 模式视为与范围生命周期管理有关的全部内容。您希望尽可能最后获得资源,尽可能快地使用并尽快释放。 using 语句通过确保即使出现异常也会调用Dispose() 来帮助执行此操作。

    【讨论】:

    • .Dispose() 方法 与析构函数密切相关 - finalizer 是不同之处。您使用 Dispose 进行与析构函数相同的 RAII 样式确定性释放,例如C++。
    【解决方案5】:

    @Atario,不仅接受的答案是错误的,您自己的编辑也是如此。想象以下情况(实际发生在 Visual Studio 2005 的一个 CTP 中):

    对于绘制图形,您无需丢弃它们即可创建笔。笔不需要大量内存,但它们在内部使用 GDI+ 句柄。如果不丢弃笔,GDI+ 手柄将不会松开。如果您的应用程序不是内存密集型应用程序,则可以在不调用 GC 的情况下经过相当长的一段时间。但是,可用 GDI+ 句柄的数量受到限制,很快,当您尝试创建新笔时,操作将失败。

    事实上,在 Visual Studio 2005 CTP 中,如果您使用该应用程序的时间足够长,所有字体都会突然切换到“系统”。

    这正是依赖 GC 进行处置不够的原因。内存使用不一定与您获取(并且不释放)的非托管资源的数量相关。因此,这些资源可能在调用 GC 之前很久就耗尽了。

    此外,当然,这些资源可能具有的所有副作用(例如访问锁)会阻止其他应用程序正常工作。

    【讨论】:

      【解决方案6】:

      这就是为什么(恕我直言)C++ 的 RAII 优于 .NET 的 using 语句。

      很多人说IDisposable 仅适用于非托管资源,这取决于您如何定义“资源”。您可以拥有一个实现IDisposable 的读/写锁,然后“资源”是对代码块的概念访问。您可以有一个对象,在构造函数中将光标更改为沙漏,然后返回到IDispose 中先前保存的值,然后“资源”是更改后的光标。我会说,当您希望在离开范围时发生确定性操作时,无论范围如何离开,您都可以使用 IDisposable,但我不得不承认,它远不如说“它用于管理非托管资源管理”那么吸引人。

      另见关于why there's no RAII in .NET的问题。

      【讨论】:

      • 终结器用于非托管资源管理; IDisposable 用于确定性 资源管理。通常对于非托管资源,您都需要 - 但可以独立于非托管代码使用 IDisposable。
      • 您不应该使用 IDisposable 来更改光标或其他任何东西。它纯粹是为了释放非托管资源。正如 Eric Lippert 所说,您“在标签外使用它”。阅读他的 cmetsblogs.msdn.com/ericlippert/archive/2009/03/06/…
      • 游标在什么方面不是“非托管资源”?每个控件都有一个,当您更改它时,您正在与其他想要更改它的人竞争,如果它“泄漏”,光标仍处于错误状态。
      【解决方案7】:

      不幸的是,FxCop 或 StyleCop 似乎都没有对此发出警告。正如其他评论者所提到的,确保调用 dispose 通常非常重要。如果我不确定,我总是检查对象浏览器 (Ctrl+Alt+J) 来查看继承树。

      【讨论】:

        【解决方案8】:

        我主要在这种情况下使用 using 块:

        我正在使用一些外部对象(在我的情况下通常是一个 IDisposable 包装的 COM 对象)。对象本身的状态可能会导致它抛出异常,或者它如何影响我的代码可能会导致我抛出异常,并且可能在许多不同的地方。一般来说,我不相信我当前方法之外的任何代码都能自行运行。

        为了争论,假设我的方法有 11 个退出点,其中 10 个在这个 using 块内,1 个在它之后(这在我编写的一些库代码中很典型)。

        由于对象在退出 using 块时会自动释放,因此我不需要进行 10 次不同的 .Dispose() 调用——它只是发生了。这会导致代码更简洁,因为它现在不再因为 dispose 调用而变得混乱(在这种情况下,代码行数减少了约 10 行)。

        如果有人在我之后更改代码,如果他们忘记调用 dispose,引入 IDisposable 泄漏错误(这可能会很耗时)的风险也较小,因为使用块没有必要。

        【讨论】:

          【解决方案9】:

          对于 Using 块的一般用法,我真的没有什么要补充的,只是想为规则添加一个例外:

          任何实现 IDisposable 的对象显然不应该在其 Dispose() 方法期间抛出异常。这在 WCF(可能还有其他)之前工作得很好,现在 WCF 通道可能在 Dispose() 期间引发异常。如果在 Using 块中使用它时发生这种情况,则会导致问题,并且需要实现异常处理。这显然需要更多关于内部工作的知识,这就是为什么微软现在建议不要在 Using blocks 中使用 WCF 通道(抱歉找不到链接,但在 Google 中有很多其他结果),即使它实现了 IDisposable.. 只是为了让事情更多复杂!

          【讨论】:

            【解决方案10】:

            简而言之,如果我错过了一次使用,这并不是世界末日。我只是希望某些东西至少会产生一个警告。

            这里的问题是您不能总是通过将 IDisposable 包装在 using 块中来处理它。有时您需要让对象停留更长时间。在这种情况下,您必须自己显式调用其Dispose 方法。

            一个很好的例子是 class uses a private EventWaitHandle (or an AutoResetEvent) 在两个线程之间进行通信,并且您希望在线程完成后处置 WaitHandle。

            因此,它不像某些工具那样简单,只是检查您是否只在 using 块内创建 IDisposable 对象。

            【讨论】:

              【解决方案11】:

              根据this linkCodeRush add-in 将在您键入时实时检测并标记本地 IDisposable 变量未清理的时间。

              可以在你的任务中遇到你。

              【讨论】:

                【解决方案12】:

                我不明白你的问题的重点。多亏了垃圾收集器,内存泄漏几乎不可能发生。但是,您需要一些稳健的逻辑。

                我用这样的方式创建IDisposable 类:

                public MyClass: IDisposable
                {
                
                    private bool _disposed = false;
                
                    //Destructor
                    ~MyClass()
                    { Dispose(false); }
                
                    public void Dispose()
                    { Dispose(true); }
                
                    private void Dispose(bool disposing)
                    {
                        if (_disposed) return;
                        GC.SuppressFinalize(this);
                
                        /* actions to always perform */
                
                        if (disposing) { /* actions to be performed when Dispose() is called */ }
                
                        _disposed=true;
                }
                

                现在,即使您错过了使用using 语句,该对象最终也会被垃圾回收并执行适当的销毁逻辑。您可以停止线程、结束连接、保存数据,无论您需要什么(在this example,我取消订阅远程服务并在需要时执行远程删除调用)

                [Edit] 显然,尽快调用 Dispose 有助于提高应用程序性能,一个很好的做法。但是,感谢我的示例,如果您忘记调用 Dispose,最终将调用它并清理对象。

                【讨论】:

                • -1 用于不太安全的链接示例。该代码至少有 2 个问题 - 1) 托管对象正在从终结器线程访问,因为它不区分正确的可处置和终结 2) 它不是线程安全的,因为 dispose 方法不会自动测试和设置已处置标志 (并且由于问题 #1,由于终结器在其自己的线程上运行,因此代码已成为多线程)
                • 是的。在您在答案中发布的代码中,您有一个检查 if(disposing) { } 但在您链接到它的代码中没有进行此类检查。因此,无论是由程序员正确处理它还是由调用终结器的垃圾收集器调用它,它都会做同样的事情。
                • 事实上,程序员的意图(我的!)在这两种情况下都做同样的事情。该文件调用 Web 服务以在 UDP 消息泵超时之前终止订阅。我链接代码不是为了显示提到的 if 语句的示例,而是为了显示应该如何使用终结器在业务逻辑级别处理资源。您提到了从终结器线程访问的托管对象:它还有什么问题而不是并发性?
                • 我又看了一遍你的例子。忽略由于终结器竞争条件导致的潜在崩溃,还有另一个关键问题。 ClientBase 类不管理任何本机资源(例如 GDI 句柄、套接字等)。终结器的唯一目的是释放原生资源。如果您不释放本机资源,则不应该有终结器。 ChannelManager 可能拥有本机资源,因此 ChannelManager 可能实现终结器。但ClientBase 并不直接拥有原生资源,因此不应实现终结器。
                • 微软为什么不应该在终结器中触摸托管对象的原因在这里简要解释msdn.microsoft.com/en-us/library/ddae83kx.aspxJoe Duffy 的优秀文章为我阐明了这些要点。 bluebytesoftware.com/blog/…
                猜你喜欢
                • 2016-08-20
                • 2010-12-06
                • 1970-01-01
                • 1970-01-01
                • 2012-12-21
                • 1970-01-01
                • 1970-01-01
                • 2020-03-05
                • 1970-01-01
                相关资源
                最近更新 更多