C#:IDisposable 和 析构函数
C# 中有两种释放资源的方式:实现 IDisposable 或使用析构函数。通常,必须在特定时间释放资源的场景中,我们实现 IDisposable,像这样:
public class ExampleDispose : IDisposable
{
// 非托管资源
private IntPtr _handle;
// 使用的其它托管资源
private readonly Stream _stream;
private bool disposed = false;
public ExampleDispose(Stream stream, IntPtr handle)
{
this._stream = stream;
this._handle = handle;
}
public void Dispose()
{
if (disposed)
{
return;
}
disposed = true;
// 调用其它托管资源的Dispose
_stream.Dispose();
// 释放非托管资源
CloseHandle(_handle);
_handle = IntPtr.Zero;
}
[System.Runtime.InteropServices.DllImport("Kernel32")]
private extern static Boolean CloseHandle(IntPtr handle);
}
通常来说,如果你在使用完后正确调用了 Dispose() 方法或者使用了 using,就可以正确释放资源。
其他情况,则需要依赖 GC 在回收托管对象前先释放引用的非托管资源,一个实现了 IDisposable 接口和具有析构函数的类可能如下所示:
public class ExampleDispose : IDisposable
{
// 非托管资源
private IntPtr _handle;
// 使用的其它托管资源
private readonly Stream _stream;
private bool disposed = false;
public ExampleDispose(Stream stream, IntPtr handle)
{
this._stream = stream;
this._handle = handle;
}
~ExampleDispose()
{
// 此代码有异常情况,不要在生产环境使用
Dispose();
}
public void Dispose()
{
if (disposed)
{
return;
}
disposed = true;
// 调用其它托管资源的Dispose
_stream.Dispose();
// 释放非托管资源
CloseHandle(_handle);
_handle = IntPtr.Zero;
}
[System.Runtime.InteropServices.DllImport("Kernel32")]
private extern static Boolean CloseHandle(IntPtr handle);
}
相比上部分代码,只是多增加了一个析构函数 ~ExampleDispose(),仔细观察它非常有意思,构造函数用于对象的创建,析构函数有点像构造函数,只是没有访问修饰符,且前面多了个 “~”,我们知道这个符号通常用于取反操作,构造函数取反,那就是“销毁函数”,也正如它所隐含的含义那样:GC 会在回收对象前调用析构函数来释放资源。现在,无论用户是否主动调用过 Dispose(),~ExampleDispose() 都会在 GC 回收对象时被调用,如果用户主动释放过资源,那么 disposed 为 true,~ExampleDispose() 实际上调用 Dispose() 后直接返回,没有多于的操作,但是另一种情况,用户从未主动调用过 Dispose(),这时候在 GC 在调用 ~ExampleDispose() 时,会调用到 _stream.Dispose();。注意,这里可能会产问题,因为 GC 在回收对象时,对象内部其它对象可能已经被回收,这里 _stream 是有可能已经被回收的,因此,我们需要稍微更改一下,以实现在 GC 时不引用任何托管代码,为此,我们实现 dispose pattern:
public class ExampleDispose : IDisposable
{
// 非托管资源
private IntPtr _handle;
// 使用的其它托管资源
private readonly Stream _stream;
private bool disposed = false;
public ExampleDispose(Stream stream, IntPtr handle)
{
this._stream = stream;
this._handle = handle;
}
~ExampleDispose()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
}
// 如果 disposing 为 true, 则这是用户代码主动释放,
// 可以安全的释放托管与非托管对象
// 反之,则为 `GC` 回收对象时调用,这时不可引用任何托管对象
protected virtual void Dispose(bool disposing)
{
if (disposed)
{
return;
}
disposed = true;
if (disposing)
{
// 调用其它托管资源的Dispose
_stream.Dispose();
}
// 释放非托管资源
CloseHandle(_handle);
_handle = IntPtr.Zero;
}
[System.Runtime.InteropServices.DllImport("Kernel32")]
private extern static Boolean CloseHandle(IntPtr handle);
}
这样看起来,它已经能够正常工作了,但是还有个小问题:即使用户主动释放了资源,GC 还是会对 ~ExampleDispose() 进行调用。我们再对代码进行一次小更改,实现用户主动释放资源后,使得 GC 回收对象时不再调用 ~ExampleDispose():
public class ExampleDispose : IDisposable
{
// 非托管资源
private IntPtr _handle;
// 使用的其它托管资源
private readonly Stream _stream;
private bool disposed = false;
public ExampleDispose(Stream stream, IntPtr handle)
{
this._stream = stream;
this._handle = handle;
}
~ExampleDispose()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
// 调用 GC.SuppressFinalize 将该对象从终结队列中移除,
// 并防止该对象的终结代码再次执行。
GC.SuppressFinalize(this);
}
// 如果 disposing 为 true, 则这是用户代码主动释放,
// 可以安全的释放托管与非托管对象
// 反之,则为 `GC` 回收对象时调用,这时不可引用任何托管对象
protected virtual void Dispose(bool disposing)
{
if (disposed)
{
return;
}
disposed = true;
if (disposing)
{
// 调用其它托管资源的Dispose
_stream.Dispose();
}
// 释放非托管资源
CloseHandle(_handle);
_handle = IntPtr.Zero;
}
[System.Runtime.InteropServices.DllImport("Kernel32")]
private extern static Boolean CloseHandle(IntPtr handle);
}