net C# 如何理解和实现 Dispose 方法
1、接口 IDisposable接口 IDisposable 包含了一个名为 Dispose 的方法。namespace System; public interface IDisposable { void Dispose(); }实现了接口 IDisposable 的类才可以使用 using 语句进行调用以实现资源的释放否则将报错错提示using 语句中使用的类型必须实现System.IDisposable。public class DbHelper : IDisposable { //... public void Dispose() { //... } } // 使用 using 语句进行调用 using (var helper new DbHelper()) { //... } // 使用 using 语句编译后等价于 DbHelper helper null; try { helper new DbHelper(); //... } finally { helper?.Dispose(); }2、析构函数C# 编译器不会为没有显式定义析构函数的类自动生成析构函数。只有当你显式定义了析构函数~ClassName()时编译器才会生成 Finalize 方法【析构函数是 C# 语法糖最终编译为 Finalize 方法】。析构函数的调用时机不可控。析构函数将仅用于释放非拖管资源。拖管资源由 GC 进行释放释放时机和顺序不可控。若拖管资源再由析构函数来释放则可能导致程序崩溃已释放的资源不存在的资源在析构函数中再次被释放。功能上析构函数与 Dispose 方法重复进行了相同的工作。但二者角色不同在实际的开发实践中析构函数通常用于对忘记主动调用 Dispose 方法的补救。因此最佳实践是将释放工作交给另一个方法void Dispose(bool disposing)然后通过disposing进行区分到底调用是来自 Dispose 方法还是来自析构函数。示例如下public class DbHelper : IDisposable { //... public void Dispose() { Dispose(true); } protected virtual void Dispose(bool disposing) { if (disposing) { // 释放托管资源 // ... } // 释放非托管资源无论手动/GC 都必须执行 // ... } ~DbHelper() { Dispose(false); } }3、实现幂等所谓幂等即函数被调用次数不同不影响结果。即无论调用多少次结果完全一样不会报错、不会崩溃。Dispose 方法必须实现幂等因为代码里可能不小心多次调用 Dispose 方法。如果不做幂等程序会报错、崩溃、资源重复释放。最佳实践中资源释放工作交给了void Dispose(bool disposing)因此void Dispose(bool disposing)实现幂等即可。实现幂等的方式比较简单即如果已经释放资源则不再进行资源释放工作通过字段private bool _disposed false;来实现。public class DbHelper : IDisposable { //... public void Dispose() { Dispose(true); } // 实现幂等的关键字段 private bool _disposed false; protected virtual void Dispose(bool disposing) { //如果已经释放资源则不再进行资源释放工作 if (_disposed) { return; } if (disposing) { // 释放托管资源 // ... } // 释放非托管资源无论手动/GC 都必须执行 // ... _disposed true; } ~DbHelper() { Dispose(false); } }4、继承时的资源释放继承时子类是需要释放父类实现中所占用的资源。因此void Dispose(bool disposing)最好应用protected virtual进行修饰以便子类调用。另子类的 void Dispose(bool disposing) 也应具有幂等特性。在该方法上父类与子类应各自维护自己的幂等特性。当类不需要被继承时 sealed 类可以简化 Dispose 模式即不需要用 virtual 修饰方法。以下是子类示例public class Derived : DbHelper { // 其他 Derived 类特有的成员 // 注意不要重新实现 Dispose() 方法因为基类已经实现了 IDisposable // 如果重新实现会隐藏基类的 Dispose()导致通过基类引用和子类引用调用 Dispose() 时行为不一致破坏多态性。 // 只需重写 void Dispose(bool disposing) 方法即可 //public void Dispose() //{ // Dispose(true); //} // 子类也应使 override 的 Dispose(bool disposing)实现幂等 private bool _disposed false; protected override void Dispose(bool disposing) { //如果已经释放资源则不再进行资源释放工作 if (_disposed) { return; } if (disposing) { // 释放 Derived 类特有的托管资源 // ... } // 释放 Derived 类特有的非托管资源无论手动/GC 都必须执行 // ... // 调用基类的 Dispose 方法确保基类资源也得到释放 base.Dispose(disposing); _disposed true; } // 子类不必实现析构函数因为父类有析构函数。在析构的过程中子类析构函数执行先于父类析构函数。 // 但无论如何父类析构函数总会执行又因为 override 了 void Dispose(bool disposing) 的缘故 // 父类的析构函数中 Dispose(false) 语句由于类的多态特性将调用子类的 void Dispose(bool disposing) 方法。 // 仅调用 Dispose(false) 的子类析构函数是冗余的。 //~Derived() //{ // Dispose(false); //} }5、性能改善有析构函数的对象会被放入 Finalization 队列增加 GC 负担。Dispose 方法用于主动地立即地资源释放当我们主动释放资源后垃圾回收器并不知情因此需要知会垃圾回收器请求不必调用终结器。当类没有实现析构函数不通过其进行兜底时则不必知会垃圾回收器。如果没有析构函数GC.SuppressFinalize(this);则是无意义的。高性能场景避免使用析构函数。public void Dispose() { Dispose(true); // 请求系统不要调用这个对象的终结器以提高性能 GC.SuppressFinalize(this); }6、其他注意事项1异常处理禁止在 Dispose 中抛异常。因为可能会导致编译生成的等效的 finally 块执行中断进而资源泄漏违背释放资源的初衷。2设置为 null纯托管对象如 ListT不需要手动释放GC 会自动回收设置为 null 不会立即释放内存但可以断开引用帮助 GC 更早发现对象不可达。其他的如实现 IDisposable 的托管对象、非托管资源以及静态资源等直接设置为 null 是无效的释放资源。3字段 _disposed它作为对象的字段存在布尔类型在系统调用析构函数时它仍然没有消失仍处理生命周期内。4ObjectDisposedException主动调用 Dispose 方法后对象可能还在其生命周期中。如果此时调用对象的其他方法则可能产生未知的错误。因此需要在其他方法中检查是否已经释放资源。public class DbHelper : IDisposable { private bool _disposed false; public void ExecuteQuery(string sql) { if (_disposed) { throw new ObjectDisposedException(nameof(DbHelper)); } // 正常执行逻辑 } }如果可行最好在调用 Dispose 方法后立即将对象设置为 null。if (disposing) { _managedResource?.Dispose(); _managedResource null; }5线程安全当需要时void Dispose(bool disposing)应实现其线程安全避免多线程同时进入该方法从而导致错误发生。protected virtual void Dispose(bool disposing) { lock (_lockObject) { if (_disposed) { return; } if (disposing) { // 释放 Derived 类特有的托管资源 // ... } // 释放 Derived 类特有的非托管资源 // ... base.Dispose(disposing); _disposed true; } }除使用上述锁方式外还可使用 Interlocked 进行。// 使用 Interlocked 的轻量级实现 private int _disposed 0; protected virtual void Dispose(bool disposing) { if (Interlocked.Exchange(ref _disposed, 1) 0) { if (disposing) { // 释放托管资源 } // 释放 Derived 类特有的非托管资源 // ... // 释放非托管资源 base.Dispose(disposing); } }6异步释放.NET Core 3.0 引入了异步释放模式对于需要异步释放资源的场景如异步文件操作、网络连接等非常重要。通过实现接口 IAsyncDisposable 的 DisposeAsync 方法的方式提供了一种异步释放非托管资源的机制。关于 异步释放资源的实现 与注意事项此处略。namespace System { public interface IAsyncDisposable { // 返回结果: 一个 task 用于异步释放操作. ValueTask DisposeAsync();// 非托管资源释放、异步释放或重置 } }7一般情况一般来说实现了接口IDisposable 的类的实例其释放应放在disposing true中进行因为它们已具有析构函数进行兜底。例如数据库的连接的关闭。部分类如 DbConnection有 Close 方法本质是 Dispose 的封装。if (disposing) { _managedResource?.Dispose(); _managedResource null; SqliteConn.Close(); }7、完整示例public class DbHelper : IDisposable { //... public void ExecuteQuery(string sql) { if (_disposed) { throw new ObjectDisposedException(nameof(DbHelper)); } // 正常执行逻辑 } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } private bool _disposed false; protected readonly object _lockObject new object(); protected virtual void Dispose(bool disposing) { lock (_lockObject) { if (_disposed) { return; } if (disposing) { // 释放托管资源 // ... } // 释放非托管资源 // ... _disposed true; } } ~DbHelper() { Dispose(false); } } public class Derived : DbHelper { // 其他 Derived 类特有的成员 //... private bool _disposed false; protected override void Dispose(bool disposing) { //基类和子类使用相同的锁对象保证整个释放过程是原子的 lock (_lockObject) { if (_disposed) { return; } if (disposing) { // 释放 Derived 类特有的托管资源 // ... } // 释放 Derived 类特有的非托管资源 // ... base.Dispose(disposing); _disposed true; } } }