通过阅读Microsoft文档,我知道IDisposable接口的“主要”用途是清理非托管资源。

对我来说,“非托管”意味着数据库连接、套接字、窗口句柄等。但是,我看到过一些代码,其中使用Dispose()方法来释放托管资源,这对我来说似乎是多余的,因为垃圾收集器应该为你负责。

例如:

public class MyCollection : IDisposable
{
    private List<String> _theList = new List<String>();
    private Dictionary<String, Point> _theDict = new Dictionary<String, Point>();

    // Die, clear it up! (free unmanaged resources)
    public void Dispose()
    {
        _theList.clear();
        _theDict.clear();
        _theList = null;
        _theDict = null;
    }
}

我的问题是,这是否会使MyCollection使用的垃圾收集器释放内存比正常情况更快?


编辑:到目前为止,人们已经发布了一些使用IDisposable清理非托管资源(如数据库连接和位图)的好例子。但假设上述代码中的_theList包含一百万个字符串,并且您希望现在释放内存,而不是等待垃圾收集器。上面的代码能做到这一点吗?


当前回答

在对对象调用Dispose后,不应再调用该对象的方法(尽管对象应允许进一步调用Dispose)。因此,问题中的例子是愚蠢的。如果调用Dispose,则可以丢弃对象本身。因此,用户只需丢弃对整个对象的所有引用(将其设置为null),其内部的所有相关对象将自动被清理。

关于托管/非托管的一般性问题以及其他答案中的讨论,我认为对这个问题的任何回答都必须从非托管资源的定义开始。

归根结底,你可以调用一个函数来让系统进入一种状态,而你可以调用另一个函数来使它恢复到那种状态。现在,在典型示例中,第一个可能是返回文件句柄的函数,第二个可能是对CloseHandle的调用。

但是-这是关键-它们可以是任何匹配的函数对。一个建立一个国家,另一个摧毁它。如果状态已构建但尚未拆除,则资源的实例存在。您必须安排在正确的时间进行拆卸-资源不由CLR管理。唯一自动管理的资源类型是内存。有两种:GC和堆栈。值类型由堆栈管理(或通过在引用类型内搭便车),引用类型由GC管理。

这些函数可能会导致可以自由交错的状态变化,或者可能需要完美嵌套。状态更改可能是线程安全的,也可能不是。

看看Justice问题中的例子。对日志文件缩进的更改必须完全嵌套,否则会出错。此外,它们也不太可能是线程安全的。

可以搭上垃圾收集器的便车来清理未管理的资源。但前提是状态更改函数是线程安全的,并且两个状态的生存期可以以任何方式重叠。因此,正义的资源示例不能有终结器!这对任何人都没有帮助。

对于这些类型的资源,您可以只实现IDisposable,而不需要终结器。终结器是绝对可选的-它必须是。这在很多书中都被掩盖了,甚至没有提到。

然后,您必须使用using语句来确保调用Dispose。这本质上就像在堆栈上搭车一样(因为终结器是在GC上,而使用是在堆栈上)。

缺少的部分是您必须手动编写Dispose,并将其调用到字段和基类中。C++/CLI程序员不必这样做。在大多数情况下,编译器会为它们编写代码。

还有一种选择,我更喜欢嵌套完美且不线程安全的状态(除了其他之外,避免IDisposable避免了与无法抗拒为每个实现IDisposale的类添加终结器的人发生争论的问题)。

不用编写类,而是编写函数。该函数接受要回调的委托:

public static void Indented(this Log log, Action action)
{
    log.Indent();
    try
    {
        action();
    }
    finally
    {
        log.Outdent();
    }
}

然后一个简单的例子是:

Log.Write("Message at the top");
Log.Indented(() =>
{
    Log.Write("And this is indented");

    Log.Indented(() =>
    {
        Log.Write("This is even more indented");
    });
});
Log.Write("Back at the outermost level again");

传入的lambda作为一个代码块,因此就像您创建自己的控制结构以实现与使用相同的目的,只是您不再有调用方滥用它的危险。它们不可能不清理资源。

如果资源的生存期可能重叠,那么这种技术就不太有用了,因为这样你就可以构建资源A,然后构建资源B,然后杀死资源A,再杀死资源B。但是,您需要使用IDisposable(但仍然没有终结器,除非您实现了线程安全,这不是免费的)。

其他回答

是的,这段代码完全是多余和不必要的,它不会让垃圾回收器做任何它不会做的事情(即,一旦MyCollection的实例超出范围),尤其是.Clear()调用。

对编辑的回答:有点。如果我这样做:

public void WasteMemory()
{
    var instance = new MyCollection(); // this one has no Dispose() method
    instance.FillItWithAMillionStrings();
}

// 1 million strings are in memory, but marked for reclamation by the GC

在内存管理方面,它的功能与此相同:

public void WasteMemory()
{
    var instance = new MyCollection(); // this one has your Dispose()
    instance.FillItWithAMillionStrings();
    instance.Dispose();
}

// 1 million strings are in memory, but marked for reclamation by the GC

如果您真的真的需要立即释放内存,请调用GC.Collect()。内存将在需要时释放。

我看到很多答案都转向了讨论对托管和非托管资源使用IDisposable。我认为这篇文章是我找到的关于如何实际使用IDisposable的最佳解释之一。

https://www.codeproject.com/Articles/29534/IDisposable-What-Your-Mother-Never-Told-You-About

对于实际问题;如果您使用IDisposable清理占用大量内存的托管对象,简短的答案是否定的。原因是,一旦持有内存的对象超出范围,它就可以进行收集了。此时,任何引用的子对象也超出范围,将被收集。

唯一真正的例外是,如果托管对象中占用了大量内存,并且您已经阻止了该线程等待某个操作完成。如果在调用完成后不需要这些对象,那么将这些引用设置为null可能会让垃圾收集器更快地收集它们。但那个场景将代表需要重构的坏代码——而不是IDisposable的用例。

大多数关于“非托管资源”的讨论的一个问题是,它们并没有真正定义这个术语,但似乎暗示它与非托管代码有关。虽然许多类型的非托管资源确实与非托管代码交互,但用这种术语来思考非托管资源是没有帮助的。

相反,我们应该认识到所有被管理的资源都有共同点:它们都包含一个对象,要求某个外部“事物”代表它做一些事情,而损害了其他一些“事物”,而另一个实体同意这样做,直到另行通知。如果物体被抛弃并消失得无影无踪,那么任何东西都不会告诉外部的“事物”,它不再需要代表不再存在的物体改变自己的行为;因此,“东西”的有用性将永久性地降低。

因此,非托管资源表示外部“事物”代表对象改变其行为的协议,如果对象被放弃并不再存在,这将毫无用处地损害外部“事物的有用性。被管理的资源是一个对象,它是此类协议的受益人,但如果它被放弃,它已签署接收通知,并将使用该通知在其被销毁之前整理其事务。

IDisposable通常用于利用using语句,并利用一种简单的方法对托管对象进行确定性清理。

public class LoggingContext : IDisposable {
    public Finicky(string name) {
        Log.Write("Entering Log Context {0}", name);
        Log.Indent();
    }
    public void Dispose() {
        Log.Outdent();
    }

    public static void Main() {
        Log.Write("Some initial stuff.");
        try {
            using(new LoggingContext()) {
                Log.Write("Some stuff inside the context.");
                throw new Exception();
            }
        } catch {
            Log.Write("Man, that was a heavy exception caught from inside a child logging context!");
        } finally {
            Log.Write("Some final stuff.");
        }
    }
}

如果MyCollection无论如何都将被垃圾收集,那么您不需要对其进行处理。这样做只会过度消耗CPU,甚至可能使垃圾收集器已经执行的一些预先计算的分析无效。

我使用IDisposable来执行诸如确保线程以及非托管资源被正确处理之类的操作。

编辑回应斯科特的评论:

GC性能指标唯一受到影响的时间是调用〔sic〕GC.Collect()时“

从概念上讲,GC维护对象引用图的视图,以及线程堆栈帧中对它的所有引用。这个堆可能非常大,并跨越许多页面的内存。作为优化,GC缓存对不太可能经常更改的页面的分析,以避免不必要地重新扫描页面。当页面中的数据发生更改时,GC从内核接收通知,因此它知道页面已脏,需要重新扫描。如果集合在Gen0中,那么页面中的其他内容也可能发生变化,但在Gen1和Gen2中这种情况发生的可能性较小。有趣的是,对于将GC移植到Mac以使Silverlight插件在该平台上工作的团队来说,这些钩子在Mac OS X中不可用。

反对不必要的资源处置的另一点是:想象一个进程正在卸载的情况。想象一下,这个过程已经运行了一段时间。很可能该进程的许多内存页已被交换到磁盘。至少它们不再位于L1或L2缓存中。在这种情况下,正在卸载的应用程序没有必要将所有数据和代码页交换回内存,以“释放”进程终止时操作系统无论如何都会释放的资源。这适用于托管资源,甚至某些非托管资源。只有使非后台线程保持活动状态的资源才能被释放,否则进程将保持活动状态。

现在,在正常执行期间,必须正确清理临时资源(正如@fezmonkey指出的数据库连接、套接字和窗口句柄),以避免非托管内存泄漏。这些都是必须处理的事情。如果您创建了一个拥有线程的类(我的意思是它创建了线程,因此负责确保它停止,至少按照我的编码风格),那么该类很可能必须实现IDisposable,并在Dispose期间拆除线程。

.NET框架使用IDisposable接口作为开发人员必须释放此类的信号,甚至是警告。我想不出框架中实现IDisposable的任何类型(不包括显式接口实现),其中处置是可选的。