C# Dispose模式
C# 中的 Dispose 模式是实现 IDisposable 的接口并释放类中持有的非托管资源的机制,这个模式的目的的是为了及时释放宝贵的非托管资源和托管资源,并且保证资源在被gc回收的时候可以正确释放资源,同时兼顾执行效率。
编写Dispose模式时应考虑以下问题
- 什么时候需要调用finalizer?
- 什么情况下需要虚拟Dispose方法?
- 如果已经调用过了Dispose方法怎么处理?
- 调用派生类上的Dispose方法怎么确保基类的资源被正确释放?
例子一,只包含托管资源
public sealed class MyDisposableClass : IDisposable
{
private readonly SqlConnection _sqlConnection;
private bool _alreadyDisposed;
public MyDisposableClass(string connectionString)
{
_sqlConnection = new SqlConnection(connectionString);
}
public void MyClassPublicMethod()
{
if (_alreadyDisposed)
{
throw new ObjectDisposedException(nameof(MyDisposableClass));
}
// Method implementation
}
public void Dispose()
{
if(_alreadyDisposed)
return;
_sqlConnection.Dispose();
_alreadyDisposed = true;
}
}
以上方法包含了一个_alreadyDisposed字段来指示如果_sqlConnection数据库连接已经释放过一次则不再进行释放,确保不会多次调用到产生报错,但是这个示例中只涉及了托管资源的释放所以这里没有编写终结器相关的代码(Finalizers)
当前仅当代码中涉及到了非托管资源的存在时才需要终结器(Finalizers)的存在
例子二,包含托管资源和非托管资源
public sealed class MyDisposableClass : IDisposable
{
private readonly SqlConnection _sqlConnection;
private readonly IntPtr _unmanagedPointer;
private bool _alreadyDisposed;
public MyDisposableClass(string connectionString)
{
_sqlConnection = new SqlConnection(connectionString);
_unmanagedPointer = Marshal.AllocHGlobal(100 * 1024 * 1024);
}
public void MyClassPublicMethod()
{
if (_alreadyDisposed)
{
throw new ObjectDisposedException(nameof(MyDisposableClass));
}
// Method implementation
}
public void Dispose()
{
if(_alreadyDisposed)
return;
_sqlConnection.Dispose();
_alreadyDisposed = true;
Marshal.FreeHGlobal(_unmanagedPointer);
GC.SuppressFinalize(this);
}
~MyDisposableClass()
{
if(_alreadyDisposed)
return;
Marshal.FreeHGlobal(_unmanagedPointer);
}
}
这个例子代码中包含了一个指向 100 MB 非托管内存块的指针,这个指针假设是和外界进行代码交互用的,不属于托管资源,不会被.NET GC正确释放,在这里我们可以看到在两个地方清理了非托管内存 - 在 Dispose() 方法和 ~MyDisposableClass() 终结器中。如果调用者正常调用了 Dispose() 方法,所有资源将立即被清理。此时,已经完成了处理,因此我们通过调用 GC.SuppressFinalize(this) 指示 GC 不要运行终结器。但是为了预防因为某些原因忘记调用 Dispose(),资源清理将在将来某个时候 GC 调用我们的终结器时去释放对应的资源。
为什么我们终结器中处理 SqlConnection 资源呢?垃圾对象的终结器可以以完全不确定的方式以任何顺序运行。根本无法保证两个超出范围的对象之间哪个终结器将首先执行。这意味着当我们的终结器被调用时,SqlConnection 可能已经被终结。这就是为什么在终结器中,我们只能处理 GC 不知道如何处理的非托管资源。此时,托管对象不在我们的控制范围内。
将以上代码做一定的整合,则
例子三
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if(_alreadyDisposed)
return;
if (disposing)
{
_sqlConnection.Dispose();
_alreadyDisposed = true;
}
Marshal.FreeHGlobal(_unmanagedPointer);
}
~MyDisposableClass()
{
Dispose(false);
}
通过两个bool标志位来确保多次调用的情况下能正常工作。
如果考虑继承并正常处理基类的情况的话,则可以将可以将Dispose设置成虚方法让整个继承链下的类自行释放自身的资源
例子四
public class MyDerivedDisposableClass : MyDisposableClass
{
private readonly FileStream _fileStream;
private readonly IntPtr _unmanagedPointer;
private bool _alreadyDisposed;
public MyDerivedDisposableClass(string path, string connectionString) : base(connectionString)
{
_fileStream = new FileStream(path, FileMode.Open);
_unmanagedPointer = Marshal.AllocHGlobal(100 * 1024 * 1024);
}
public void MyDerivedClassPublicMethod()
{
if (_alreadyDisposed)
{
throw new ObjectDisposedException(nameof(MyDerivedDisposableClass));
}
// 做一些其他事情
}
protected override void Dispose(bool disposing)
{
if(_alreadyDisposed)
return;
if (disposing)
{
_fileStream.Dispose();
}
Marshal.FreeHGlobal(_unmanagedPointer);
_alreadyDisposed = true;
base.Dispose(disposing); // 调用父类的处理方法,释放父类的资源
}
~MyDerivedDisposableClass()
{
Dispose(false);
}
}
完整的调用流程如下
- 调用公共 Dispose() 方法,例如IDisposable 实现。该方法是派生类公共接口的一部分,因为它是从基类继承的。
- Dispose() 调用受保护的虚拟 Dispose(disposing) 方法,并设置 disposing=true。此时,由于多态性,该方法的派生类实现被调用。
- 派生类 Dispose(disposing)负责释放所有托管和非托管资源。
- 派生类调用父类实现Dispose(disposing)。
- 基类清理其托管和非托管资源。
- 派生类和基类 Dispose(dispose) 执行都已完成,程序流程返回到公共 Dispose() 方法。
- Dispose() 调用 GC.SupressFinalize(this) 并执行完成。