文章目录
Object.Finalize 方法
定义 Definition
名称空间: System
程序集:System.Runtime.dll
允许对象在被垃圾收集(Garbage Collection)回收之前尝试释放资源和执行其它清理操作。
~Object();
析构函数(destructor)与终结器(Finalizer)
本文明明讲的是Finalize方法,在MSDN文档的示例代码却是一个析构函数,这是为什么?
《C#高级编程》17.4.1中提到,在讨论C#中的析构函数时,在底层的.NET体系结构中,该函数被称为终结器(finalizer),在C#中定义析构函数时,编译器发送给程序集的实际上是Finalize()方法,它不会影响源代码。如果查看生成的IL代码,就会知道这个事实。
我下面写了个简单的示例,用ILDasm反编译进行实验,public class Desx { ~Desx() { MessageBox.Show("Destructor"); } }
将上面代码编译生成的dll再反编译,可以看到了Desx类中出现了Finalize方法,析构函数却消失了。
这说明,C#编译器在编译析构函数时,它会隐式地把析构函数的代码编译为等价于重写Finalize()方法的代码。
还可以看到,析构函数中实现的代码封装在了一个Finalize()方法的try块中,且父类(Object)的Finalize()调用放在finally块中。
示例 Examples
以下示例验证当重写了 Finalize 方法的对象被销毁时是否会调用 Finalize 方法。注意,在生产应用程序中, Finalize 方法被重写以释放对象所持有的非托管资源(unmanaged resources)。另注意,C#示例提供了析构函数(destructor)以代替重写 Finalize 方法(即上小节末提到的)。
析构函数
正如《C#高级编程》17.4.1中提到的,C++开发人员应该更熟悉析构函数,它看起来类似于一个方法,与包含的类同名。但有一个前缀波形符(~)。它没有返回类型、参数、访问修饰符。
有经验的C++开发人员会大量使用析构函数,有时不仅用于清理资源,还提供调试信息或执行其他任务。C#析构函数比C++使用少很多。与C++析构函数相比,C#析构函数的问题是它们的不确定性。在销毁C++对象时,其析构函数会立即运行。但由于C# GC的工作方式,无法确定C#对象的析构函数何时进行。所以,不能在析构函数中放置需要在某一时刻运行的代码,也不应寄希望析构函数会以特定顺序来对不同类的实例调用。若对象占用了重要的资源,应尽快释放这些资源,而不是等待GC释放。
另一个问题是C#析构函数的实现会延迟对象最终从内存中删除的时间。没有析构函数的对象会在GC的一次处理中从内存中删除,但有析构函数的对象需要处理两次才能销毁;第一次调用析构函数时,没有删除对象,第二次调用才真正删除对象。此外,运行库使用一个线程来执行所有对象的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
有关重写 Finalize 方法的其它示例,参阅 GC.SuppressFinalize 方法。
备注 Remarks
Finalize 方法用于在对象被销毁之前对当前对象持有的非托管资源执行清理操作。该方法是受保护的(protected),因此只能通过此类或其派生类访问。
下面几节中有介绍:
- finalization如何工作
- 实现注意
- SafeHandle替代方案
终结是如何工作的 How finalization works
Object 类不提供 Finalize 方法的实现,并且垃圾收集器(Garbage Collector,简称GC)不会将从 Object 派生的类型标记为 finalization(终止、终结) 除非它们重写 Finalize 方法。
若某个类型确实重写了 Finalize 方法,则GC会将该类型的每个实例项添加到一个叫 finalization queue(终结队列) 的内部结构中。finalization queue 包含托管堆中所有对象的项,这些对象的 finalization 代码必须在GC回收它们的内存前运行。GC在以下环境下会自动调用 Finalize 方法:
- 在GC发现某个对象无法访问后,除非该对象已通过调用 GC.SuppressFinalize 方法免除 Finalization 。
- 仅在 .NET Framework上,在应用程序域(application domain)关闭期间,除非该对象免除 finalization 。在关闭期间,甚至仍可访问的对象也会被终止。
Finalize 在给定实例上只会自动调用一次,除非通过使用诸如 GC.ReRegisterForFinalize 和 GC.SuppressFinalize 等机制重新注册该对象,并且之后未调用 GC.SuppressFinalize 方法。
Finalize 操作有以下限制:
- finalizer(终结器) 执行的确切时间是未定义的。要确保类实例资源的确定性释放,实现 Close 方法或提供 IDisposable.Dispose 实现。
- 即使一个对象引用了另一个对象,也无法保证两个对象的终结器以任何特定顺序运行。也就是说,如果 ObjectA 引用了 ObjectB ,且两者都有终结器,则当 ObjectA 的终结器启动时, ObjectB 可能已经被终结。
- 终结器运行的线程是未指定的。
在以下特殊情况下,Finalize 方法可能无法运行完成或根本不运行:
- 如果另一个终结器无限期地阻塞(进入无限循环,尝试获取它永远无法获取的锁,等等)。由于运行时会尝试运行终结器直至完成,因此如果终结器无限期阻塞,则可能不会调用其他终结器。
- 若进程在没有给运行时机会进行清理的情况下终止了。在该情况下,运行时的第一个进程终止通知是 DLL_PROCESS_DETACH 通知。
仅当可终结对象的数量继续减少时,运行时才会在关闭期间继续终结对象。
如果 Finalize 或 Finalize 的重写引发了异常,且运行时不是由重写默认策略的应用程序托管,则运行时将终止进程,并且不会执行任何活动的 try/finally 块或终结器。
如果终结器无法释放或销毁资源,此行为可确保进程完整性。
重写终结方法 Overriding the Finalize method
对于使用非托管资源(例如文件句柄或数据库连接)的类,你应该重写 Finalize ,当使用这些资源的托管对象在垃圾收集期间被丢弃时,必须释放这些资源。你不应该为托管对象实现 Finalize 方法,因为GC会自动释放托管资源。
重要
若可以使用 SafeHandle 对象来包装非托管资源,则建议用SafeHandle实现dispose模式来替代重写Finalize。
默认情况下, Object.Finalize 方法不执行任何操作,但你应仅在需要时和在释放非托管资源时才重写Finalize。若运行终结操作,回收内存往往需要更长的时间,因为它至少需要两次垃圾回收。此外,你应该仅重写引用类型的Finalize方法。CLR仅终结引用类型,它会忽略值类型的终结器。
Object.Finalize 方法的作用域是 protected 。当你重写类中方法时,你应该保持这个作用域。通过保持Finalize方法的protected作用域,可以防止程序的用户直接调用Finalize方法。
派生类型中的Finalize的每个实现都必须调用其基类型的Finalize实现。这是允许应用程序代码调用的Finalize的唯一情况。对象的Finalize方法不应调用除其基类之外的任何对象上的方法。这是因为被调用的其他对象可能与调用对象同时被回收,例如在CLR关闭的情况下。
⭐注意
C#编译器不允许你重写Finalize方法。取而代之的是,你可以通过为类实现析构函数来提供终结器。C#析构函数会自动调用其基类的析构函数。
不过,VC++提供了自己的语法来实现Finalize方法。详细信息,参阅 如何:定义和使用类与结构体(C++/CLI)中的""析构函数和终结器"部分
由于垃圾回收是不确定的(non-deterministic),因此你无法准确知道GC何时执行终结。
要立即释放资源,你也可以选择实现dispose模式和IDisposable接口。类的使用者可调用 IDisposable.Dispose 实现来释放非托管资源,并且你可以使用Finalize方法释放非托管资源,在Dispose 方法未被调用的情况下。
Finalize几乎可以执行任何操作,包括在垃圾回收期间清理对象后复活对象(即使对象再次能被访问)。但是,该对象只能复活一次;在垃圾回收期间,无法对复活的对象调用 Finalize。
🔺SafeHandle替代方案 The SafeHandle alternative
创建可靠的终结器通常很困难,因为你无法假设应用程序的状态,并且因为未处理的系统异常(如OutOfMemoryException和StackOverflowException)会终止终结器。你可以使用从 System.Runtime.InteropServices.SafeHandle 类派生的对象来包装非托管资源,然后在不使用终结器的情况下实现dispose模式。.NET Framework在Microso.Win32名称空间中提供了派生自 System.Runtime.InteropServices.SafeHandle 的以下类:
- SafeFileHandle是一个文件句柄的包装类。
- SafeMemoryMappedFileHandle是内存映射文件句柄的包装类。
- SafeMemoryMappedViewHandle 是非托管内存指针的包装类。
- SafeNCryptKeyHandle、SafeNCryptProviderHandle和SafeNCryptSecretHandle是加密句柄的包装类。
- SafePipeHandle是管道句柄的包装类。
- SafeRegistryHandle是注册表句柄的包装类。
- SafeWaitHandle是等待句柄的包装类。
下面示例使用具有安全句柄的dispose模式来替代重写Finalize的方案。它定义了一个 FileAssociation 类,该类包装有关处理特定文件扩展名的文件的应用程序注册表信息。两个注册表句柄由Windows RegOpenKeyEx调用返回为 out 参数传给 SafeRegistryHandle构造函数。接着,该类型的protected Dispose方法调用 SafeRegistryHandle.Dispose 方法来释放这两个句柄。
using Microsoft.Win32.SafeHandles;
using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
public class FileAssociationInfo : IDisposable
{
// Private variables
private String ext;
private String openCmd;
private String args;
private SafeRegistryHandle hExtHandle, hAppIdHandle;
// Windows API Calls
[DllImport("advapi32.dll", CharSet= CharSet.Auto, SetLastError=true)]
private static extern int RegOpenKeyEx(IntPtr hKey,
String lpSubKey, int ulOptions, int samDesired,
out IntPtr phkResult);
[DllImport("advapi32.dll", CharSet= CharSet.Unicode, EntryPoint = "RegQueryValueExW",
SetLastError=true)]
private static extern int RegQueryValueEx(IntPtr hKey,
string lpValueName, int lpReserved, out uint lpType,
string lpData, ref uint lpcbData);
[DllImport("advapi32.dll", SetLastError = true)]
private static extern int RegSetValueEx(IntPtr hKey, [MarshalAs(UnmanagedType.LPStr)] string lpValueName,
int Reserved, uint dwType, [MarshalAs(UnmanagedType.LPStr)] string lpData,
int cpData);
[DllImport("advapi32.dll", SetLastError=true)]
private static extern int RegCloseKey(UIntPtr hKey);
// Windows API constants.
private const int HKEY_CLASSES_ROOT = unchecked((int) 0x80000000);
private const int ERROR_SUCCESS = 0;
private const int KEY_QUERY_VALUE = 1;
private const int KEY_SET_VALUE = 0x2;
private const uint REG_SZ = 1;
private const int MAX_PATH = 260;
public FileAssociationInfo(String fileExtension)
{
int retVal = 0;
uint lpType = 0;
if (!fileExtension.StartsWith("."))
fileExtension = "." + fileExtension;
ext = fileExtension;
IntPtr hExtension = IntPtr.Zero;
// Get the file extension value.
retVal = RegOpenKeyEx(new IntPtr(HKEY_CLASSES_ROOT), fileExtension, 0, KEY_QUERY_VALUE, out hExtension);
if (retVal != ERROR_SUCCESS)
throw new Win32Exception(retVal);
// Instantiate the first SafeRegistryHandle.
hExtHandle = new SafeRegistryHandle(hExtension, true);
string appId = new string(' ', MAX_PATH);
uint appIdLength = (uint) appId.Length;
retVal = RegQueryValueEx(hExtHandle.DangerousGetHandle(), String.Empty, 0, out lpType, appId, ref appIdLength);
if (retVal != ERROR_SUCCESS)
throw new Win32Exception(retVal);
// We no longer need the hExtension handle.
hExtHandle.Dispose();
// Determine the number of characters without the terminating null.
appId = appId.Substring(0, (int) appIdLength / 2 - 1) + @"\shell\open\Command";
// Open the application identifier key.
string exeName = new string(' ', MAX_PATH);
uint exeNameLength = (uint) exeName.Length;
IntPtr hAppId;
retVal = RegOpenKeyEx(new IntPtr(HKEY_CLASSES_ROOT), appId, 0, KEY_QUERY_VALUE | KEY_SET_VALUE,
out hAppId);
if (retVal != ERROR_SUCCESS)
throw new Win32Exception(retVal);
// Instantiate the second SafeRegistryHandle.
hAppIdHandle = new SafeRegistryHandle(hAppId, true);
// Get the executable name for this file type.
string exePath = new string(' ', MAX_PATH);
uint exePathLength = (uint) exePath.Length;
retVal = RegQueryValueEx(hAppIdHandle.DangerousGetHandle(), String.Empty, 0, out lpType, exePath, ref exePathLength);
if (retVal != ERROR_SUCCESS)
throw new Win32Exception(retVal);
// Determine the number of characters without the terminating null.
exePath = exePath.Substring(0, (int) exePathLength / 2 - 1);
// Remove any environment strings.
exePath = Environment.ExpandEnvironmentVariables(exePath);
int position = exePath.IndexOf('%');
if (position >= 0) {
args = exePath.Substring(position);
// Remove command line parameters ('%0', etc.).
exePath = exePath.Substring(0, position).Trim();
}
openCmd = exePath;
}
public String Extension
{ get { return ext; } }
public String Open
{ get { return openCmd; }
set {
if (hAppIdHandle.IsInvalid | hAppIdHandle.IsClosed)
throw new InvalidOperationException("Cannot write to registry key.");
if (! File.Exists(value)) {
string message = String.Format("'{0}' does not exist", value);
throw new FileNotFoundException(message);
}
string cmd = value + " %1";
int retVal = RegSetValueEx(hAppIdHandle.DangerousGetHandle(), String.Empty, 0,
REG_SZ, value, value.Length + 1);
if (retVal != ERROR_SUCCESS)
throw new Win32Exception(retVal);
} }
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
protected void Dispose(bool disposing)
{
// Ordinarily, we release unmanaged resources here;
// but all are wrapped by safe handles.
// Release disposable objects.
if (disposing) {
if (hExtHandle != null) hExtHandle.Dispose();
if (hAppIdHandle != null) hAppIdHandle.Dispose();
}
}
}
}