如何在.NET中销毁一个类?

45

.NET垃圾回收器最终会释放内存,但是如果您想立即获得该内存怎么办?在类 MyClass 中调用什么代码?

MyClass.Dispose()

如何释放MyClass中变量和对象占用的所有空间?

20个回答

103

IDisposable与释放内存无关。IDisposable是一种用于释放未托管资源的模式,而内存绝对是托管资源。

指向GC.Collect()的链接是正确的答案,但Microsoft .NET文档通常不鼓励使用此函数。

编辑:因为我为这个答案赚取了大量Karma,所以我感觉有责任对其进行阐述,以免.NET资源管理的新手产生错误印象。

在.NET进程内,有两种资源——托管和未托管。 "托管"表示运行时控制资源,而"未托管"则意味着由程序员负责。 在.NET今天真正关心的托管资源中,确实只有一种——内存。程序员告诉运行时分配内存,之后由运行时决定何时释放内存。 .NET用于此目的的机制称为垃圾回收,使用谷歌就可以在互联网上找到大量关于GC的信息。

对于其他类型的资源,.NET不知道如何清理它们,因此必须依靠程序员做正确的事情。 为此,平台为程序员提供了三个工具:

  1. IDisposable接口和VB和C#中的 "using"语句
  2. Finalizers
  3. 由许多BCL类实现的IDisposable模式

其中第一个允许程序员在同一方法中高效地获取资源、使用它并释放它。

using (DisposableObject tmp = DisposableObject.AcquireResource()) {
    // Do something with tmp
}
// At this point, tmp.Dispose() will automatically have been called
// BUT, tmp may still a perfectly valid object that still takes up memory

如果“AcquireResource”是一个工厂方法,例如打开文件,而“Dispose”会自动关闭该文件,那么这段代码就不会泄漏文件资源。但是,“tmp”对象本身的内存可能仍然被分配。这是因为IDisposable接口与垃圾收集器没有任何联系。如果您确实希望确保释放了内存,则唯一的选择是调用GC.Collect()强制进行垃圾回收。

然而,无论如何都不能强调这不是一个好主意。最好让垃圾收集器做它设计的事情,即管理内存。

如果资源被使用的时间更长,其寿命跨越多个方法,会发生什么?显然,“using”语句不再适用,因此当程序员完成资源使用时,必须手动调用“Dispose”。如果程序员忘记了怎么办?如果没有后备方案,那么进程或计算机最终可能会耗尽未正确释放的资源。

这就是finalizer的作用。finalizer是类上的一个方法,与垃圾收集器有特殊关系。GC承诺-在释放该类型任何对象的内存之前-首先给finalizer一个机会进行某种清理。

因此,在文件的情况下,理论上我们根本不需要手动关闭文件。我们可以等待垃圾收集器到达并让finalizer完成工作。不幸的是,这在实践中效果不佳,因为垃圾收集器运行是非确定性的。文件可能会保持打开状态比程序员预期时间更长。如果保持打开的文件足够多,当尝试打开另一个文件时,系统可能会失败。

对于大多数资源,我们想要这两个东西。我们想要一种约定,以便能够说“我们现在已经完成了这个资源”,并且我们想确保即使忘记手动清理,也至少有一些机会自动进行清理。这就是IDisposable模式发挥作用的地方。这是一种约定,允许IDispose和finalizer友好地协作。您可以查看IDisposable官方文档了解该模式的工作原理。

最重要的是:如果您真正想做的就是确保内存被释放,那么IDisposable和finalizer将无法帮助您。但是,IDisposable接口是所有.NET程序员都应该了解的极其重要的模式的一部分。


我只是想指出这个问题及其答案存在术语问题。你们都在谈论释放/处理对象,即类的实例。一个类是类型本身,相关代码-过程、函数、属性等-以及共享(静态)类数据。释放类本身的内存或其他资源比释放实例的内存更困难,因为它涉及到卸载整个应用程序域,包括其程序集和其他相关类。当然,这不是人们通常想做的事情。 - Guillermo Prandi
@Curl,如果您能够包含一些代码示例,那就太好了。 - T.Todua

25

只有实现IDisposable接口的实例才能被处理。

强制进行垃圾回收以立即释放(非托管的)内存:

GC.Collect();  
GC.WaitForPendingFinalizers();

通常情况下这是不好的做法,但例如在.NET框架的x64版中存在一个bug,可能会导致GC在某些场景下表现异常,那么您可能需要这样做。我不知道这个bug是否已经解决了。有人知道吗?

要释放一个类,您可以这样做:

instance.Dispose();

或者像这样:

using(MyClass instance = new MyClass())
{
    // Your cool code.
}

这将在编译时转换为:

MyClass instance = null;    

try
{
    instance = new MyClass();        
    // Your cool code.
}
finally
{
    if(instance != null)
        instance.Dispose();
}

你可以这样实现IDisposable接口:

public class MyClass : IDisposable
{
    private bool disposed;

    /// <summary>
    /// Construction
    /// </summary>
    public MyClass()
    {
    }

    /// <summary>
    /// Destructor
    /// </summary>
    ~MyClass()
    {
        this.Dispose(false);
    }

    /// <summary>
    /// The dispose method that implements IDisposable.
    /// </summary>
    public void Dispose()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }

    /// <summary>
    /// The virtual dispose method that allows
    /// classes inherithed from this one to dispose their resources.
    /// </summary>
    /// <param name="disposing"></param>
    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                // Dispose managed resources here.
            }

            // Dispose unmanaged resources here.
        }

        disposed = true;
    }
}

这与我正在尝试解决的问题类似。如果您有托管代码... MyClass,您想要表明您已经完成了它。您只需分配为null吗?您是说这与实现IDisposable相同吗?很难解释我要问什么。基本上,我有一个循环,每次循环都会新建一个类,我想表明我已经完成了它。我应该保持原样还是分配为null,或者实现没有所有其他位(终结器)的using语句。空的dispose方法。 这样做是错误的吗? - Seabizkit
@Seabizkit,只有当你的类持有其他需要IDisposable(并调用其Dispose方法)的东西以及使用非托管资源等内容时,才应该实现IDisposable。如果你担心在循环中的GC成本,也许你应该考虑将其作为值类型(结构体)或通过改变其工作方式来重复使用实例。 - Patrik Svensson

20
这个问题的回答已经变得有点混乱了。
标题问的是处理,但接着又说他们想要立即恢复内存。
.Net是“托管”的,这意味着当您编写.Net应用程序时,您不需要直接担心内存,代价是您也无法直接控制内存。
.Net决定何时清理和释放内存,而不是作为.Net编码器的您。
“Dispose”是一种告诉.Net您已经完成某些操作的方法,但它实际上不会释放内存,直到最佳时机才会这样做。
基本上,.Net将在最容易的时候收回内存-它非常擅长决定何时这样做。除非您正在编写非常占用内存的内容,否则通常不需要覆盖它(这是游戏尚未经常使用.Net编写的原因之一-它们需要完全控制)
在.Net中,您可以使用“GC.Collect()”来强制它立即执行,但这几乎总是不好的做法。如果.Net还没有清理它,那么这意味着现在不是特别好的时间。
“GC.Collect()”会获取由.Net标识为已完成的对象。如果您没有处置需要处置的对象,则.Net可能决定保留该对象。这意味着只有在正确实现可处置实例时,“GC.Collect()”才是有效的。
“GC.Collect()”不能替代正确使用IDisposable。
因此,Dispose和内存并不直接相关,但它们不需要这样。正确处置将使您的.Net应用程序更加高效,从而使用更少的内存。
在.Net中,99%的时间最佳实践如下:
规则1:如果您没有处理任何未经管理或实现IDisposable的内容,则无需担心Dispose。
规则2:如果您有一个实现IDisposable的局部变量,请确保在当前范围内摆脱它:
//using is best practice
using( SqlConnection con = new SqlConnection("my con str" ) )
{
    //do stuff
} 

//this is what 'using' actually compiles to:
SqlConnection con = new SqlConnection("my con str" ) ;
try
{
    //do stuff
}
finally
{
    con.Dispose();
}

规则3:如果一个类有实现IDisposable接口的属性或成员变量,则该类也应该实现IDisposable接口。在该类的Dispose方法中,你还可以处理你的IDisposable属性:

//rather basic example
public sealed MyClass :
   IDisposable
{   
    //this connection is disposable
    public SqlConnection MyConnection { get; set; }

    //make sure this gets rid of it too
    public Dispose() 
    {
        //if we still have a connection dispose it
        if( MyConnection != null )
            MyConnection.Dispose();

        //note that the connection might have already been disposed
        //always write disposals so that they can be called again
    }
}

这并不完整,这就是为什么示例被封装的原因。继承类可能需要遵守下一个规则...
规则4:如果一个类使用了未管理的资源,则实现IDispose并添加终结器。
.Net不能对未管理的资源做任何操作,所以现在我们谈论的是内存。如果您不清理它,您可能会得到内存泄漏。
Dispose方法需要处理受控和未受控的资源。
终结器是一种安全保障 - 它确保如果有人创建了您的类的实例并且未能处理它,'危险的'未管理资源仍然可以被.Net清理。
~MyClass()
{
    //calls a protected method 
    //the false tells this method
    //not to bother with managed
    //resources
    this.Dispose(false);
}

public void Dispose()
{
    //calls the same method
    //passed true to tell it to
    //clean up managed and unmanaged 
    this.Dispose(true);

    //as dispose has been correctly
    //called we don't need the 

    //'backup' finaliser
    GC.SuppressFinalize(this);
}

最后是带有布尔标志的Dispose的重载:

protected virtual void Dispose(bool disposing)
{
    //check this hasn't been called already
    //remember that Dispose can be called again
    if (!disposed)
    {
        //this is passed true in the regular Dispose
        if (disposing)
        {
            // Dispose managed resources here.
        }

        //both regular Dispose and the finaliser
        //will hit this code
        // Dispose unmanaged resources here.
    }

    disposed = true;
}

请注意,一旦所有内容都准备就绪,创建类实例的其他托管代码只需像处理其他IDisposable一样处理它(规则2和3)。

14

是否适当提到,dispose 不仅仅指内存?我更经常处理文件引用等资源的处理。GC.Collect() 与CLR垃圾回收器直接相关,可能会释放内存(在任务管理器中)。它很可能会对应用程序产生负面影响(例如性能)。

总之,为什么您希望立即回收内存呢?如果来自其他地方的内存压力存在,大多数情况下操作系统会为您获取内存。


6
请看这篇文章:article 实现Dispose模式、IDisposable和/或终结器与内存何时被回收没有任何关系;相反,它与告诉GC如何回收该内存有关。当您调用Dispose()时,您并没有与GC交互。
GC仅在确定需要运行(称为内存压力)时才会运行,然后(仅在这种情况下)将释放未使用对象的内存并压缩内存空间。
您可以调用GC.Collect(),但除非有非常好的原因(几乎总是“从不”),否则您真的不应该这样做。当您强制进行此类带外收集周期时,您实际上会导致GC做更多的工作,并最终可能损害应用程序的性能。在GC收集周期期间,您的应用程序实际上处于冻结状态……运行的GC周期越多,您的应用程序花费的时间就越长。
您还可以进行一些本机Win32 API调用以释放工作集,但即使是这些调用也应该避免,除非有非常好的原因要这样做。
垃圾收集运行时背后的整个前提是,您不需要过多地担心运行时何时分配/释放实际内存;您只需要确保您的对象知道如何在被要求时清理自己即可。

5
我在http://codingcraftsman.wordpress.com/2012/04/25/to-dispose-or-not-to-dispose/上写了一个有关析构函数、Dispose和垃圾回收的摘要。
回答原始问题:
  1. 不要尝试管理内存
  2. Dispose 不是关于内存管理的,而是关于非托管资源管理的
  3. Finalizers 是 Dispose 模式的固有部分,并且实际上会减慢托管对象的内存释放速度(因为它们必须进入终结队列,除非已经被 Dispose)
  4. GC.Collect 不好,因为它使一些短暂存在的对象看起来需要更长时间,从而减慢了它们被收集的速度。
然而,在性能关键的代码段中,GC.Collect 可能是有用的,如果你想减少垃圾回收减速的可能性,可以在之前调用它。
此外,还有一个支持这种模式的论点:
var myBigObject = new MyBigObject(1);
// something happens
myBigObject = new MyBigObject(2);
// at the above line, there are temporarily two big objects in memory and neither can be collected

vs

myBigObject = null; // so it could now be collected
myBigObject = new MyBigObject(2);

但主要的答案是垃圾回收只要不乱搞就可以正常工作!

3
public class MyClass : IDisposable
{
    public void Dispose()
    {
       // cleanup here
    }
}

那么你可以像这样做
MyClass todispose = new MyClass();
todispose.Dispose(); // instance is disposed right here

或者

using (MyClass instance = new MyClass())
{

}
// instance will be disposed right here as it goes out of scope

3

Joe Duffy对“Dispose,Finalization和Resource Management”进行了完整的解释:

在.NET Framework的早期,C#程序员通常将finalizers称为析构函数。随着时间的推移,我们正试图认识到Dispose方法实际上更类似于C++析构函数(确定性),而finalizer是完全不同的东西(非确定性)。C#借鉴了C++析构函数语法(即~T()),这种误称的发展肯定至少与此有点关系。


2

虽然有方法可以强制运行垃圾回收,但你并不能真正强制清除想要的对象。最好的方法是使用try catch ex finally dispose end try(VB.NET规则)调用Dispose来清理由对象分配的系统资源(内存、句柄、数据库连接等)。但Dispose仅能以确定性方式清理系统资源,无法清理对象本身使用的内存,只有垃圾回收器才能做到。


1

这篇文章提供了一个非常简单的指南。然而,需要调用GC而不是让它自然运行通常意味着糟糕的设计/内存管理,尤其是如果没有消耗有限资源(连接、句柄或其他通常导致实现IDisposable的东西)。

是什么导致你需要这样做呢?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接