C# 析构函数(Destructor)和终结器(Finalizer)——托管资源的释放
本文内容
- 使用析构函数释放资源
- Object.Finalize 方法
- 资源的显式释放
使用析构函数释放资源
析构函数用于析构类的实例。
- 不能在结构中定义析构函数。只能对类使用析构函数。
- 一个类只能有一个析构函数。
- 无法继承或重载析构函数。
- 无法调用析构函数。它们是被自动调用的。
- 析构函数既没有修饰符,也没有参数。
示例 1:类 Car 析构函数的声明。
class Car
{
/// <summary>
/// 析构函数
/// </summary>
~Car()
{
// cleanup statements...
}
}
该析构函数隐式调用对象基类的 Finalize 方法。因此,该析构函数被隐式地转换为如下代码:
protected override void Finalize()
{
try
{
// Cleanup statements...
}
finally
{
base.Finalize();
}
}
这意味着,对继承链中的所有实例递归调用 Finalize 方法。
说明:不要使用空的析构函数。如果类包含析构函数,则 Finalize 队列中则会创建一个项。当调用析构函数时,将调用垃圾回收器(GC)来处理该队列。如果析构函数为空,只会导致不必要的性能损失。
程序员无法控制何时调用析构函数,因为这由垃圾回收器决定。垃圾回收器检查是否存在应用程序不再使用的对象。如果垃圾回收器认为某个对象符合析构,则调用析构函数(如果有的话),回收该对象的内存。程序退出时也会调用析构函数。
可以通过调用 Collect 强制进行垃圾回收,但大多数情况下应避免这样做,因为会导致性能问题。
通常,.NET Framework 垃圾回收器会隐式地管理对象的内存分配和释放。但当应用程序封装窗口、文件
和网络连接这类非托管资源时,应使用析构函数释放这些资源。当对象符合析构时,垃圾回收器将运行对象的 Finalize 方法。
虽然垃圾回收器可以跟踪封装非托管资源的对象的生存期,但它不了解具体如何清理这些资源。常见的非托管源有:ApplicationContext、Brush、Component、ComponentDesigner、Container、Context、Cursor、FileStream、Font、Icon、Image、Matrix、Object、OdbcDataReader、OleDBDataReader、Pen、Regex、Socket、StreamWriter、Timer、Tooltip 等。
Object.Finalize 方法
允许 Object 在“垃圾回收”回收 Object 之前,尝试释放资源并执行其他清理操作。Finalize 是受保护的,因此只能通过此类或派生类访问它。
对象变为不可访问后,将自动调用此方法,除非已通过 GC.SuppressFinalize 调用使对象免除了终结。在应用程序域的关闭过程中,对没有免除终结的对象将自动调用 Finalize,即使那些对象仍是可访问的。对于给定的实例仅自动调用 Finalize 一次,除非使用 GC.ReRegisterForFinalize重新注册该对象,并且后面没有调用 GC.SuppressFinalize。
派生类型中的每个 Finalize 实现都必须调用其基类型的 Finalize 实现。这是唯一一种允许应用程序代码调用 Finalize 的情况。
注意:C# 编译器不允许你直接实现 Finalize 方法,因此 C# 析构函数自动调用其基类的析构函数。
Finalize 操作具有下列限制:
1) 垃圾回收过程中执行终结器的准确时间是不确定的。不保证资源在任何特定的时间都能释放,除非调用 Close 方法或 Dispose 方法。
2) 即使一个对象引用另一个对象,也不能保证两个对象的终结器以任何特定的顺序运行。即,如果对象 A 具有对对象 B 的引用,并且两者都有终结器,则当对象 A 的终结器启动时,对象 B 可能已经终结了。
3) 运行终结器的线程是未指定的。
在下面的异常情况下,Finalize 方法可能不会运行完成或可能根本不运行:
1) 另一个终结器无限期地阻止(进入无限循环,试图获取永远无法获取的锁,诸如此类)。由于运行时试图运行终结器来完成,所以如果一个终结器无限期地阻止,则可能不会调用其他终结器。
2) 进程终止,但不给运行时提供清理的机会。在这种情况下,运行时的第一个进程终止通知是 DLL_PROCESS_DETACH 通知。
在关闭过程中,只有当可终结对象的数目继续减少时,运行时才继续 Finalize 对象。
如果 Finalize 或 Finalize 的重写引发异常,并且运行库并非寄宿在重写默认策略的应用程序中,则运行库将终止进程,并且不执行任何活动的 try-finally 块或终结器。如果终结器无法释放或销毁资源,此行为可以确保进程完整性。
说明:默认情况下,Object.Finalize 不执行任何操作。只有在必要时才必须由派生类重写它,因为如果必须运行 Finalize 操作,垃圾回收过程中的回收往往需要长得多的时间。如果 Object 保存了对任何资源的引用,则 Finalize 必须由派生类重写,以便在垃圾回收过程中,在放弃 Object 之前释放这些资源。当类型使用文件句柄或数据库连接这类在回收使用托管对象时必须释放的非托管资源时,该类型必须实现 Finalize。Finalize 可以采取任何操作,包括在垃圾回收过程中清理了对象后使对象复活(即,使对象再次可访问)。但是,对象只能复活一次;在垃圾回收过程中,不能对复活对象调用 Finalize。
析构函数是执行清理操作的 C# 机制。析构函数提供了适当的保护措施,如自动调用基类型的析构函数。在 C# 代码中,不能调用或重写 Object.Finalize。
示例 2:当一个重构 Finalize 方法的对象被销毁时,会调用 Finalize 方法。注意,在产品应用程序中,Finalize 方法会被重构,以便释放对象拥有的非托管资源。另外,C# 提供析构函数,而没有重构 Finalize 方法。
using System;
using System.Diagnostics;
public class ExampleClass
{
Stopwatch sw;
public ExampleClass()
{
sw = Stopwatch.StartNew();
Console.WriteLine("Instantiated object");
}
public void ShowDuration()
{
Console.WriteLine("This instance of {0} has been in existence for {1}", this, sw.Elapsed);
}
~ExampleClass()
{
Console.WriteLine("Finalizing object");
sw.Stop();
Console.WriteLine("This instance of {0} has been in existence for {1}", this, sw.Elapsed);
}
}
public class Demo
{
public static void Main()
{
ExampleClass ex = new ExampleClass();
ex.ShowDuration();
}
}
输出结果如下:
Instantiated object
This instance of ExampleClass has been in existence for 00:00:00.0011060
Finalizing object
This instance of ExampleClass has been in existence for 00:00:00.0036294
资源的显式释放
若应用程序在使用昂贵的外部资源,建议提供一种在垃圾回收器释放对象前显式地释放资源的方式。可通过实现来自 IDisposable 接口的 Dispose 方法来完成这一点,该方法为对象执行必要的清理。这样可大大提高应用程序的性能。即使有这种对资源的显式控制,析构函数也是一种保护措施,可用来在对 Dispose 方法的调用失败时清理资源。
更多信息,请参见 清理非托管资源。
示例 3:创建三个类,这三个类构成了一个继承链。这三个类都有析构函数。在 Main() 中,创建了派生程度最大的类的实例。程序运行时,这三个类的析构函数将自动被调用,并按照从派生程度最大的到派生程度最小的次序调用。
class First
{
~First()
{
System.Diagnostics.Trace.WriteLine("First's destructor is called.");
}
}
class Second : First
{
~Second()
{
System.Diagnostics.Trace.WriteLine("Second's destructor is called.");
}
}
class Third : Second
{
~Third()
{
System.Diagnostics.Trace.WriteLine("Third's destructor is called.");
}
}
class TestDestructors
{
static void Main()
{
Third t = new Third();
}
}
在VS 输出窗口(快捷键Ctrl+W,O)会看到如下信息:
……
Third's destructor is called.
Second's destructor is called.
First's destructor is called.
程序“[3796] TestDestructors.vshost.exe: 托管”已退出,返回值为0 (0x0)。
更多信息,请参见 C# 语言规范中的以下各章节:
a) 1.6.7.6 析构函数
b) 10.3.9.4 为析构函数保留的成员名称
c) 10.13 析构函数(类)
d) 11.3.9 析构函数(结构)
C# 语言规范位于 Visual Studio 2008 安装目录下的 VC#/Specifications/1033/ 目录。
参考
GC http://msdn.microsoft.com/zh-cn/library/0xy59wtx(v=VS.90).aspx