C# 清理非托管资源
本文内容
- 实现 Dispose 方法
- Finalize 方法和析构函数
- 使用封装资源的对象
- 参考
通过将对象的范围限制为 protected,以防止直接调用其 Finalize 方法。最好不要直接调用非基类类的 Finalize 方法。
为了适时释放非托管资源,建议实现公共的 Dispose 或 Close 方法。IDisposable 接口为实现接口的资源类提供 Dispose 方法。Dispose 方法是公共的,因此,应用程序用户可以直接调用该方法来释放非托管资源占用的内存。当正确实现 Dispose 方法,可未能调用 Dispose 方法时,Finalize 方法充当防护措施来清理资源。
实现 Dispose 方法
实现 Dispose 方法用于释放非托管资源。
释放对象的模式(称为释放模式 dispose pattern)对对象的生存期进行规定。释放方案仅用于非托管资源的对象。因为垃圾回收器对回收未使用的托管对象非常有效。
一个类型的 Dispose 方法应释放它拥有的所有资源,它还应该通过调用其父类型的 Dispose 方法来释放其基类型拥有的所有资源。父类型的 Dispose 方法应该释放它拥有的所有资源,进而调用其父类型的 Dispose 方法,从而在整个基类型层次结构中传播此模式。若要确保资源始终被正确地清理Dispose 方法应该可以多次调用,而不引发异常。
对只使用托管资源的类型(如数组 arrays)实现 Dispose 方法并不能提高性能,因为这些类型由垃圾回收器自动回收。应主要对使用本机资源的托管对象(如 FileStream )和向 .NET Framework 公开的 COM 对象使用 Dispose 方法。
Dispose 方法应该调用 SuppressFinalize 方法来释放对象。如果对象当前在终止队列中,则 SuppressFinalize 会阻止调用其 Finalize 方法。记住,执行 Finalize 方法会降低性能。如果Dispose 方法已完成清理对象的工作,垃圾回收器就不必调用对象的 Finalize 方法。
GC.KeepAlive 方法提供的代码示例演示了,强行垃圾回收如何在回收对象的成员仍在执行时引发终结器(finalizer)运行。在较长的 Dispose 方法最后,最好调用 KeepAlive 方法。
SafeHandle 可选
编写一个对象的终结器代码 finalizer 是一个复杂的任务,如果没有正确完成,可能会导致问题。因此,我们建议构造 SafeHandle 对象,而不是实现释放模式(dispose pattern)。
SafeHandle 类通过分配和不中断的释放句柄,简化对象生存期问题。它包含一个关键的终结器,保证在应用程序域正在卸载时运行。
SafeHandle 类位于 System.Runtime.InteropServices 命名空间,它是一个抽象的包装类,用来操作系统的句柄。从该类派生(继承)是很困难的,但可以使用 Microsoft.Win32.SafeHandles 命名空间下的派生类,它提供以下安全的句柄:
- 文件和管道
- 内存视图
- 加密构造
- 注册表项
- 等待句柄
示例
下面演示为封装非托管资源的类实现 Dispose 方法的推荐设计模式。
资源类通常是从复杂的本机类或 API 派生的,而且必须进行相应的自定义。使用这一代码模式作为创建资源类的一个起始点,并根据封装的资源提供必要的自定义。
using System;
using System.IO;
namespace AMConsoleApp
{
class Program
{
static void Main(string[] args)
{
try
{
// 初始化一个 Stream 资源,把它传给 DisposableResource 类
Console.Write("Enter filename and its path: ");
string fileSpec = Console.ReadLine();
FileStream fs = File.OpenRead(fileSpec);
DisposableResource TestObj = new DisposableResource(fs);
// 使用资源
TestObj.DoSomethingWithResource();
// 释放资源
TestObj.Dispose();
Console.ReadKey();
}
catch (FileNotFoundException e)
{
Console.WriteLine(e.Message);
Console.ReadKey();
}
}
/// <summary>
/// This class shows how to use a disposable resource.
/// The resource is first initialized and passed to the constructor, but it could also be initialized in the constructor.
/// The lifetime of the resource does not exceed the lifetime of this instance.
/// This type does not need a finalizer because it does not directly create a native resource like a file handle or memory in the unmanaged heap.
/// </summary>
public class DisposableResource : IDisposable
{
private Stream _resource;
private bool _disposed;
/// <summary>
/// 将 stream 传给构造函数,它必须可读、非空
/// </summary>
/// <param name="stream"></param>
public DisposableResource(Stream stream)
{
if (stream == null)
throw new ArgumentNullException("Stream in null.");
if (!stream.CanRead)
throw new ArgumentException("Stream must be readable.");
_resource = stream;
_disposed = false;
}
/// <summary>
/// 验证资源的使用. 它必须没有被释放
/// </summary>
public void DoSomethingWithResource()
{
if (_disposed)
throw new ObjectDisposedException("Resource was disposed.");
// 显示字节数
int numBytes = (int)_resource.Length;
Console.WriteLine("Number of bytes: {0}", numBytes.ToString());
}
public void Dispose()
{
Dispose(true);
// Use SupressFinalize in case a subclass of this type implements a finalizer.
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
// 如果需要线程安全,请在这个操作,以及使用资源方法中使用 lock
if (!_disposed)
{
if (disposing)
{
if (_resource != null)
_resource.Dispose();
Console.WriteLine("Object disposed.");
}
// Indicate that the instance has been disposed.
_resource = null;
_disposed = true;
}
}
}
}
}
说明
- 定义 DisposableResource 类,从外部获得文件流,读取其大小,最后释放其文件流的资源。该类继承 IDisposable 接口,并实现 Dispose() 方法;
- DisposableResource 类的成员函数 DoSomethingWithResource 获得文件流的大小;
- DisposableResource 类的成员函数 Dispose 极其重载,负责释放资源。
IDisposable 接口
定义一种释放分配的资源的方法。
此接口的主要用途是释放非托管资源。当不再使用托管对象时,垃圾回收器会自动释放分配给该对象的内存。但无法预测进行垃圾回收的时间。另外,垃圾回收器对窗口句柄或打开的文件和流等非托管资源一无所知。
将此接口的 Dispose 方法与垃圾回收器一起使用来显式释放非托管资源。当不再需要对象时,对象的使用者可以调用此方法。
向现有类添加 IDisposable 接口是重大的更改,因为这更改了类的语义。
有关如何使用此接口和 Object.Finalize 方法的详细讨论。
在调用可实现 IDisposable 接口的类时,请使用 try/finally 模式来确保非托管资源能够释放出来,即使应用程序因出现异常而中断也是如此。
有关 try/finally 模式的更多信息,请参见try-finally(C# 参考)或 Thetry-finally Statement。
请注意,您可以使用 using 语句(在 Visual Basic 中为 Using)来代替 try/finally 模式。有关更多信息,请参见 Using 语句 (Visual Basic) 文档或 using 语句(C# 参考)文档。
示例
下面演示如何创建用来实现 IDisposable 接口的资源类。
using System;
using System.ComponentModel;
public class DisposeExample
{
/// <summary>
/// A base class that implements IDisposable.
/// By implementing IDisposable, you are announcing that instances of this type allocate scarce resources.
/// </summary>
public class MyResource : IDisposable
{
// 指向外部非托管资源(external unmanaged resource)
private IntPtr handle;
// 类中使用其他托管资源
private Component component = new Component();
// 追踪 Dispose 是否被调用
private bool disposed = false;
public MyResource(IntPtr handle)
{
this.handle = handle;
}
/// <summary>
/// 实现 IDisposable
/// </summary>
/// <remarks>
/// 不要将该方法设置为 virtual,这样,它的子类就不会 override 这个方法
/// </remarks>
public void Dispose()
{
Dispose(true);
// This object will be cleaned up by the Dispose method.
// Therefore, you should call GC.SupressFinalize to
// take this object off the finalization queue
// and prevent finalization code for this object from executing a second time.
GC.SuppressFinalize(this);
}
/// <summary>
/// 在两个常见的情况下执行 Dispose(bool disposing)
/// </summary>
/// <remarks>
/// 如果 disposing 为 true,那么这个方法直接或间接地被用户代码调用,托管和非托管资源会被释放。
/// 如果 disposing 为 false,那么这个方法会在终结器(finalizer)内被运行时调用,你不能引用其他对象。
/// 只有非托管资源才可以被释放。
/// </remarks>
/// <param name="disposing"></param>
private void Dispose(bool disposing)
{
// 检查 Dispose 方法是否被调用
if (!this.disposed)
{
// 如果 disposing 为 true,那么释放所有的托管和非托管资源
if (disposing)
{
// 释放托管资源
component.Dispose();
}
// 调用适当的方法,清除非托管资源
// 如果 disposing 为 false,那么仅执行下面代码
CloseHandle(handle);
handle = IntPtr.Zero;
// 释放完成
disposed = true;
}
}
/// <summary>
/// 使用 interop 来调用需要清除非托管资源的方法
/// </summary>
/// <param name="handle"></param>
/// <returns></returns>
[System.Runtime.InteropServices.DllImport("Kernel32")]
private extern static Boolean CloseHandle(IntPtr handle);
/// <summary>
/// Use C# destructor syntax for finalization code.
/// </summary>
/// <remarks>
/// This destructor will run only if the Dispose method does not get called.
/// It gives your base class the opportunity to finalize.
/// Do not provide destructors in types derived from this class.
/// </remarks>
~MyResource()
{
// Do not re-create Dispose clean-up code here.
// Calling Dispose(false) is optimal in terms of readability and maintainability.
Dispose(false);
}
}
public static void Main()
{
// Insert code here to create and use the MyResource object.
}
}
Finalize 方法和析构函数
描述 Finalize 方法和析构函数如何允许一个对象在垃圾回收器自动回收对象内存之前执行必要的清理操作。
对于您应用程序创建的大多数对象,可以依靠 .NET Framework 垃圾回收器隐式地执行所有必要的内存管理任务。但是,在创建封装非托管资源的对象时,在应用程序中使用完这些非托管资源后,必须显式地释放它们。最常见的一类非托管资源就是包装操作系统资源的对象,例如文件、窗口或网络连接。虽然垃圾回收器可以跟踪封装非托管资源的对象的生存期,但它不了解具体如何清理这些资源。对于这些类型的对象,.NET Framework 提供 Object.Finalize 方法,它允许对象在垃圾回收器回收该对象使用的内存时适当清理其非托管资源。默认情况下,Finalize 方法不执行任何操作。如果您要让垃圾回收器在回收对象的内存之前对对象执行清理操作,必须在类中重写 Finalize 方法。
若要在 C# 中实现 Finalize 方法,必须使用析构函数语法。无法从 C# 或 C++ 编程语言中调用或重写 Object.Finalize 方法,C# 将析构函数用作编写终止代码的机制。
垃圾回收器使用名为“终止队列”的内部结构跟踪具有 Finalize 方法的对象。每次应用程序创建具有 Finalize 方法的对象时,垃圾回收器都在终止队列中放置一个指向该对象的项。托管堆中所有需要在垃圾回收器回收其内存之前调用它们的终止代码的对象都在终止队列中含有项。
注意:为 GC.KeepAlive 方法提供的代码示例演示,攻击性垃圾回收如何会导致终结器在已回收的对象的成员仍在执行时运行,以及如何使用 KeepAlive 方法来阻止这种情况的发生。
Finalize 方法不应引发异常,因为应用程序无法处理这些异常,而且这些异常会导致应用程序终止。
实现 Finalize 方法或析构函数对性能可能会有负面影响,应避免不必要地使用。用 Finalize 方法回收对象使用的内存,需要至少两次垃圾回收。当垃圾回收器执行回收时,它只回收没有终结器的、不可访问对象的内存,不
能回收具有终结器的、不可访问的对象。它改为将这些对象的项从终止队列中移除并将它们放置在标为准备终止的对象列表中。该列表中的项指向托管堆中准备被调用其终止代码的对象。垃圾回收器为此列表中的对象调用 Finalize 方法,然后,将这些项从列表中移除。后来的垃圾回收将确定终止的对象确实是垃圾,因为标为准备终止对象的列表中的项不再指向它们。在后来的垃圾回收中,实际上回收了对象的内存。
参考 C# 析构函数 destructor 和终结器 Finalizer
重写 Finalize 方法
描述 Finalize 和 Dispose 方法一起使用的方式。
Finalize 方法在未能调用 Dispose 方法的情况下充当防护措施来清理资源。 您应该只实现 Finalize 方法来清理非托管资源。 您不应该对托管对象实现 Finalize 方法,因为垃圾回收器会自动清理托管资源。 默认情况下,Object.Finalize 方法不进行任何操作。 如果要让垃圾回收器在回收对象的内存之前对对象执行清理操作,您必须在类中重写此方法。
若要在 C# 中实现 Finalize 方法,必须使用析构函数语法。
Object.Finalize 方法的范围是受保护的。 当在类中重写该方法时,您应该保持这个有限的范围。 通过保护 Finalize 方法,您可以防止应用程序的用户直接调用对象的 Finalize 方法。
对象的 Finalize 方法应该释放该对象保留的所有资源。 它还应该调用该对象基类的 Finalize 方法。 对象的 Finalize 方法不应对任何非其基类的对象调用方法。 这是因为被调用的其他对象可能和调用对象在同一时间被回收,例如公共语言运行时关闭这种情况。
如果您允许任何异常避开 Finalize 方法,系统将认为方法返回,并继续调用其他对象的 Finalize 方法。
C# 与 C++ 中的析构函数语法
描述 Finalize 方法在 C# 和 C++ 中的等效方法。
使用封装资源的对象
描述确保 Dispose 方法被调用的方式,例如 C# using 语句(在 Visual Basic 中为 Using)。
如果您要编写代码,而该代码使用一个封装资源的对象,您应该确保在使用完该对象时调用该对象的 Dispose 方法。 要做到这一点,可以使用 C# 的 using 语句,或使用其他面向公共语言运行时的语言来实现 try/finally 块。
Using 语句
C# 编程语言的 using 语句通过简化必须编写以便创建和清理对象的代码,使得对 Dispose 方法的调用更加自动化。 using 语句获得一个或多个资源,执行您指定的语句,然后处置对象。 请注意,using 语句只适用于这样的对象:这些对象的生存期不超过在其中构建这些对象的方法。 下面的代码示例将创建并清理 ResourceWrapper 类的实例,如 C# 示例实现Dispose 方法中所示。
class myApp
{
public static void Main()
{
using (ResourceWrapper r1 = new ResourceWrapper())
{
// Do something with the object.
r1.DoSomething();
}
}
}
与下面的代码等效。
class myApp
{
public static void Main()
{
ResourceWrapper r1 = new ResourceWrapper();
try
{
// Do something with the object.
r1.DoSomething();
}
finally
{
// Check for a null resource.
if (r1 != null)
// Call the object's Dispose method.
r1.Dispose();
}
}
}
使用 C# 的 using 语句,可以在单个语句(该语句在内部同嵌套的 using 语句是等效的)中获取多个资源。
Try/Finally 块
当您用 C# 以外的语言编写托管代码时,如果该代码使用一个封装资源的对象,请使用 try/finally 块来确保调用该对象的 Dispose 方法。下面的代码示例将创建并清理 Resource 类的实例。
class myApp
Public Shared Sub Main()
Resource r1 = new Resource()
Try
' Do something with the object.
r1.DoSomething()
Finally
' Check for a null resource.
If Not (r1 is Nothing) Then
' Call the object's Dispose method.
r1.Dispose()
End If
End Try
End SubEnd
Class