本文简述了 C# 中 Dispose 模式的一些知识
之前对 C# 中的 Dispose 模式只有些模糊印象,近来又了解了一些相关知识,在此简单做些记录~
C# 程序中每种类型都可以看做是一种资源,这些资源可以分成两类:
对于托管资源,由于受 CLR 的管理,大部分情况下我们都不用操心资源的释放问题,但是对于非托管资源,由于不受 CLR 的管理,释放的问题便必须我们自己来做了.
那么我们通过什么方法来释放这些非托管资源呢, C# 提供了一个标准接口 IDisposable :
public interface IDisposable
{
void Dispose();
}
如果你程序中的某个类型需要释放非托管资源,就让他实现 IDisposable 接口,也就是通过 void Dispose() 方法来实现非托管资源的释放, 示例代码如下:
// dispose pattern v1
public class DisposePattern : IDisposable
{
// external unmanaged resource
IntPtr m_handle;
public DisposePattern(IntPtr handle)
{
m_handle = handle;
}
public void Dispose()
{
// release external unmanaged resource
if (m_handle != IntPtr.Zero)
{
CloseHandle(m_handle);
m_handle = IntPtr.Zero;
}
}
// close handle method
[System.Runtime.InteropServices.DllImport("Kernel32")]
extern static bool CloseHandle(IntPtr handle);
}
上面的示例代码有个很大的问题,如果外部代码没有调用 DisposePattern 的 Dispose 方法,那么 DisposePattern 持有的非托管资源(m_handle)便泄露了.
就编程规范来讲,其实是应该规避外部代码不调用 Dispose 方法的行为,如果这可以做到,那么示例代码中的 Dispose 实现便已经足够了,但是这在实际中往往难以保证(或者说做到保证的成本太高),另外从实现的角度来看, DisposePattern 如果能在外部代码不调用 Dispose 方法的前提下仍然保证非托管资源不泄露,那么程序也会更加健壮.
如何实现呢?我们需要借助 C# 中的析构函数(或者叫终结器)
这里我们暂时不去关注 C# 中析构函数的各个细节,只要知道析构函数可以在类型被回收之前执行就行了,新的示例代码如下:
// dispose pattern v2
public class DisposePattern : IDisposable
{
// external unmanaged resource
IntPtr m_handle;
public DisposePattern(IntPtr handle)
{
m_handle = handle;
}
// destructor
~DisposePattern()
{
// release external unmanaged resource
if (m_handle != IntPtr.Zero)
{
CloseHandle(m_handle);
m_handle = IntPtr.Zero;
}
}
public void Dispose()
{
// release external unmanaged resource
if (m_handle != IntPtr.Zero)
{
CloseHandle(m_handle);
m_handle = IntPtr.Zero;
}
// get rid of ~DisposePattern() call
GC.SuppressFinalize(this);
}
// close handle method
[System.Runtime.InteropServices.DllImport("Kernel32")]
extern static bool CloseHandle(IntPtr handle);
}
可以看到我们额外定义了 ~DisposePattern(),并在其中实现了非托管资源的释放,这就保证了即使外部代码不调用 Dispose 方法,非托管资源也能正确释放(在 DisposePattern 回收之前),相对的,如果外部代码调用了 Dispose 方法,我们便不需要再调用 ~DisposePattern() 了(当然,这里只是说不需要,不是说不可以,这里在 Dispose 之后继续调用 ~DisposePattern() 也是可以的,这也是出于健壮性的考虑), Dispose() 方法中的 GC.SuppressFinalize(this); 便是用来"屏蔽"析构函数的执行的(定义了析构函数的类型可以通过调用 GC.SuppressFinalize 来抑制析构函数的执行).
实际的代码中,一个类型除了持有非托管资源,自然也会持有托管资源,如果这些托管资源(类型)也实现了 IDisposable 接口(或者更广义的来说,实现了 Dispose 之类的释放资源方法.这里我们将问题标准化(简化),规定实现释放资源方法就需要实现 IDisposable 接口)
最终的实现代码如下:
// dispose pattern v3
public class DisposePattern : IDisposable
{
// external unmanaged resource
IntPtr m_handle;
// managed resource
Component m_component = new Component();
// disposed flag
bool m_disposed = false;
// internal dispose method
void Dispose(bool disposing)
{
if (!m_disposed)
{
if (disposing)
{
// Dispose managed resources
m_component.Dispose();
}
// release external unmanaged resource
if (m_handle != IntPtr.Zero)
{
CloseHandle(m_handle);
m_handle = IntPtr.Zero;
}
m_disposed = true;
}
}
public DisposePattern(IntPtr handle)
{
m_handle = handle;
}
// destructor
~DisposePattern()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
// get rid of ~DisposePattern() call
GC.SuppressFinalize(this);
}
// close handle method
[System.Runtime.InteropServices.DllImport("Kernel32")]
extern static bool CloseHandle(IntPtr handle);
}
上面示例中的 DisposePattern 实现便是所谓的 Dispose 模式,代码中的几个要点还需要细细说明一下:
改善C#程序的建议4:C#中标准Dispose模式的实现
IDisposable Interface
Why using finalizers is a bad idea
Finalize and Dispose
之前我们提到, Dispose 模式中区分了 Dispose 的调用路径(如果是外部代码调用,我们一并释放托管资源和非托管资源,如果是析构函数调用,我们仅释放非托管资源),这里可以引出几个问题: