找回密码
 立即注册
首页 业界区 安全 net C# 如何理解和实现 Dispose 方法

net C# 如何理解和实现 Dispose 方法

芮梦月 4 小时前
目录

  • .net C# 如何理解和实现 Dispose 方法

    • 1、接口 IDisposable
    • 2、析构函数
    • 3、实现幂等
    • 4、继承时的资源释放
    • 5、性能改善
    • 6、其他注意事项

      • (1)异常处理
      • (2)设置为 null
      • (3)字段 _disposed
      • (4)ObjectDisposedException
      • (5)线程安全
      • (6)异步释放
      • (7)一般情况

    • 7、完整示例


.net C# 如何理解和实现 Dispose 方法

1、接口 IDisposable

接口 IDisposable 包含了一个名为 Dispose 的方法。
  1. namespace System;
  2. public interface IDisposable
  3. {
  4.     void Dispose();
  5. }
复制代码
实现了接口 IDisposable 的类,才可以使用 using 语句进行调用,以实现资源的释放,否则将报错错,提示:using 语句中使用的类型必须实现 System.IDisposable。
  1. public class DbHelper : IDisposable
  2. {
  3.     //...
  4.    
  5.     public void Dispose()
  6.     {
  7.         //...
  8.     }
  9. }
  10. // 使用 using 语句进行调用
  11. using (var helper = new DbHelper())
  12. {
  13.     //...
  14. }
  15. // 使用 using 语句编译后等价于
  16. DbHelper helper = null;
  17. try
  18. {
  19.     helper = new DbHelper();
  20.     //...
  21. }
  22. finally
  23. {
  24.     helper?.Dispose();
  25. }
复制代码
2、析构函数

C# 编译器不会为没有显式定义析构函数的类自动生成析构函数。只有当你显式定义了析构函数(~ClassName())时,编译器才会生成 Finalize 方法【析构函数是 C# 语法糖,最终编译为 Finalize 方法】。
析构函数的调用时机不可控。析构函数将仅用于释放非拖管资源。拖管资源由 GC 进行释放,释放时机和顺序不可控。若拖管资源再由析构函数来释放,则可能导致程序崩溃:已释放的资源(不存在的资源)在析构函数中再次被释放。
功能上,析构函数与 Dispose 方法重复进行了相同的工作。但二者角色不同,在实际的开发实践中,析构函数通常用于对忘记主动调用 Dispose 方法的补救。
因此,最佳实践是,将释放工作交给另一个方法 void Dispose(bool disposing),然后通过disposing 进行区分,到底调用是来自 Dispose 方法还是来自析构函数。示例如下:
  1. public class DbHelper : IDisposable
  2. {
  3.     //...
  4.    
  5.     public void Dispose()
  6.     {
  7.         Dispose(true);
  8.     }
  9.     protected virtual void Dispose(bool disposing)
  10.     {
  11.         if (disposing)
  12.         {
  13.             // 释放托管资源
  14.             // ...
  15.         }
  16.         // 释放非托管资源(无论手动/GC 都必须执行)
  17.         // ...
  18.     }
  19.     ~DbHelper()
  20.     {
  21.         Dispose(false);
  22.     }
  23. }
复制代码
3、实现幂等

所谓幂等,即函数被调用次数不同,不影响结果。即,无论调用多少次,结果完全一样,不会报错、不会崩溃。
Dispose 方法必须实现幂等,因为代码里可能不小心多次调用 Dispose 方法。如果不做幂等,程序会报错、崩溃、资源重复释放。最佳实践中,资源释放工作交给了 void Dispose(bool disposing),因此,void Dispose(bool disposing) 实现幂等即可。
实现幂等的方式,比较简单,即如果已经释放资源,则不再进行资源释放工作,通过字段 private bool _disposed = false; 来实现。
  1. public class DbHelper : IDisposable
  2. {
  3.     //...
  4.    
  5.     public void Dispose()
  6.     {
  7.         Dispose(true);
  8.     }
  9.     // 实现幂等的关键字段:
  10.     private bool _disposed = false;
  11.     protected virtual void Dispose(bool disposing)
  12.     {
  13.         //如果已经释放资源,则不再进行资源释放工作
  14.         if (_disposed) { return; }
  15.         if (disposing)
  16.         {
  17.             // 释放托管资源
  18.             // ...
  19.         }
  20.         // 释放非托管资源(无论手动/GC 都必须执行)
  21.         // ...
  22.         _disposed = true;
  23.     }
  24.     ~DbHelper()
  25.     {
  26.         Dispose(false);
  27.     }
  28. }
复制代码
4、继承时的资源释放

继承时,子类是需要释放父类实现中所占用的资源。因此,void Dispose(bool disposing) 最好应用 protected virtual 进行修饰,以便子类调用。
另,子类的 void Dispose(bool disposing) 也应具有幂等特性。在该方法上,父类与子类,应各自维护自己的幂等特性。
当类不需要被继承时( sealed 类),可以简化 Dispose 模式,即不需要用 virtual 修饰方法。
以下是子类示例:
  1. public class Derived : DbHelper
  2. {
  3.     // 其他 Derived 类特有的成员
  4.     // 注意:不要重新实现 Dispose() 方法,因为基类已经实现了 IDisposable
  5.     // 如果重新实现,会隐藏基类的 Dispose(),导致通过基类引用和子类引用调用 Dispose() 时行为不一致,破坏多态性。
  6.     // 只需重写 void Dispose(bool disposing) 方法即可
  7.     //public void Dispose()
  8.     //{
  9.     //    Dispose(true);
  10.     //}
  11.     // 子类也应使 override 的 Dispose(bool disposing)实现幂等
  12.     private bool _disposed = false;
  13.     protected override void Dispose(bool disposing)
  14.     {
  15.         //如果已经释放资源,则不再进行资源释放工作
  16.         if (_disposed) { return; }
  17.         
  18.         if (disposing)
  19.         {
  20.             // 释放 Derived 类特有的托管资源
  21.             // ...
  22.         }
  23.         // 释放 Derived 类特有的非托管资源(无论手动/GC 都必须执行)
  24.         // ...
  25.         // 调用基类的 Dispose 方法,确保基类资源也得到释放
  26.         base.Dispose(disposing);
  27.         _disposed = true;
  28.     }
  29.     // 子类不必实现析构函数,因为父类有析构函数。在析构的过程中,子类析构函数执行先于父类析构函数。
  30.     // 但无论如何,父类析构函数总会执行,又因为 override 了 void Dispose(bool disposing) 的缘故,
  31.     // 父类的析构函数中 Dispose(false) 语句,由于类的多态特性,将调用子类的 void Dispose(bool disposing) 方法。
  32.     // 仅调用 Dispose(false) 的子类析构函数,是冗余的。
  33.     //~Derived()
  34.     //{
  35.     //    Dispose(false);
  36.     //}
  37. }
复制代码
5、性能改善

有析构函数的对象会被放入 Finalization 队列,增加 GC 负担。
Dispose 方法用于主动地立即地资源释放,当我们主动释放资源后,垃圾回收器并不知情,因此需要知会垃圾回收器,请求不必调用终结器。
当类没有实现析构函数,不通过其进行兜底时,则不必知会垃圾回收器。如果没有析构函数,GC.SuppressFinalize(this);则是无意义的。
高性能场景:避免使用析构函数。
  1. public void Dispose()
  2. {
  3.     Dispose(true);
  4.    
  5.     // 请求系统不要调用这个对象的终结器,以提高性能
  6.     GC.SuppressFinalize(this);
  7. }
复制代码
6、其他注意事项

(1)异常处理

禁止在 Dispose 中抛异常。因为可能会导致(编译生成的等效的) finally 块执行中断,进而资源泄漏,违背释放资源的初衷。
(2)设置为 null

纯托管对象(如 List),不需要手动释放,GC 会自动回收,设置为 null 不会立即释放内存,但可以断开引用,帮助 GC 更早发现对象不可达。其他的,如实现 IDisposable 的托管对象、非托管资源以及静态资源等,直接设置为 null 是无效的释放资源。
(3)字段 _disposed

它作为对象的字段存在,布尔类型,在系统调用析构函数时,它仍然没有消失,仍处理生命周期内。
(4)ObjectDisposedException

主动调用 Dispose 方法后,对象可能还在其生命周期中。如果此时调用对象的其他方法,则可能产生未知的错误。因此,需要在其他方法中检查是否已经释放资源。
  1. public class DbHelper : IDisposable
  2. {
  3.     private bool _disposed = false;
  4.    
  5.     public void ExecuteQuery(string sql)
  6.     {
  7.         if (_disposed)
  8.         {
  9.             throw new ObjectDisposedException(nameof(DbHelper));
  10.         }
  11.         
  12.         // 正常执行逻辑
  13.     }
  14. }
复制代码
如果可行,最好在调用 Dispose 方法后,立即将对象设置为 null。
  1. if (disposing)
  2. {
  3.     _managedResource?.Dispose();
  4.     _managedResource = null;
  5. }
复制代码
(5)线程安全

当需要时,void Dispose(bool disposing) 应实现其线程安全,避免多线程同时进入该方法从而导致错误发生。
  1. protected virtual void Dispose(bool disposing)
  2. {
  3.     lock (_lockObject)
  4.     {
  5.         if (_disposed) { return; }
  6.         
  7.         if (disposing)
  8.         {
  9.             // 释放 Derived 类特有的托管资源
  10.             // ...
  11.         }
  12.         // 释放 Derived 类特有的非托管资源
  13.         // ...
  14.         base.Dispose(disposing);
  15.         
  16.         _disposed = true;
  17.     }
  18. }
复制代码
除使用上述锁方式外,还可使用 Interlocked 进行。
  1. // 使用 Interlocked 的轻量级实现
  2. private int _disposed = 0;
  3. protected virtual void Dispose(bool disposing)
  4. {
  5.     if (Interlocked.Exchange(ref _disposed, 1) == 0)
  6.     {
  7.         if (disposing)
  8.         {
  9.             // 释放托管资源
  10.         }
  11.         // 释放 Derived 类特有的非托管资源
  12.         // ...
  13.         // 释放非托管资源
  14.         base.Dispose(disposing);
  15.     }
  16. }
复制代码
(6)异步释放

.NET Core 3.0+ 引入了异步释放模式,对于需要异步释放资源的场景(如异步文件操作、网络连接等)非常重要。
通过实现接口 IAsyncDisposable 的 DisposeAsync 方法的方式,提供了一种异步释放非托管资源的机制。
关于 异步释放资源的实现 与注意事项,此处略。
  1. namespace System
  2. {
  3.     public interface IAsyncDisposable
  4.     {
  5.         // 返回结果: 一个 task ,用于异步释放操作.
  6.         ValueTask DisposeAsync();// 非托管资源释放、异步释放或重置
  7.     }
  8. }
复制代码
(7)一般情况

一般来说,实现了接口IDisposable 的类的实例,其释放应放在 disposing = true 中进行,因为它们已具有析构函数进行兜底。例如,数据库的连接的关闭。
部分类(如 DbConnection)有 Close 方法,本质是 Dispose 的封装。
  1. if (disposing)
  2. {
  3.     _managedResource?.Dispose();
  4.     _managedResource = null;
  5.     SqliteConn.Close();
  6. }
复制代码
7、完整示例
  1. public class DbHelper : IDisposable
  2. {
  3.     //...
  4.     public void ExecuteQuery(string sql)
  5.     {
  6.         if (_disposed)
  7.         {
  8.             throw new ObjectDisposedException(nameof(DbHelper));
  9.         }
  10.         
  11.         // 正常执行逻辑
  12.     }
  13.    
  14.     public void Dispose()
  15.     {
  16.         Dispose(true);
  17.         GC.SuppressFinalize(this);
  18.     }
  19.     private bool _disposed = false;
  20.     protected readonly object _lockObject = new object();
  21.     protected virtual void Dispose(bool disposing)
  22.     {
  23.         lock (_lockObject)
  24.         {
  25.             if (_disposed) { return; }
  26.             if (disposing)
  27.             {
  28.                 // 释放托管资源
  29.                 // ...
  30.             }
  31.             // 释放非托管资源
  32.             // ...
  33.             _disposed = true;
  34.         }
  35.     }
  36.     ~DbHelper()
  37.     {
  38.         Dispose(false);
  39.     }
  40. }
  41. public class Derived : DbHelper
  42. {
  43.     // 其他 Derived 类特有的成员
  44.     //...
  45.     private bool _disposed = false;
  46.     protected override void Dispose(bool disposing)
  47.     {
  48.         //基类和子类使用相同的锁对象,保证整个释放过程是原子的
  49.         lock (_lockObject)
  50.         {
  51.             if (_disposed) { return; }
  52.             
  53.             if (disposing)
  54.             {
  55.                 // 释放 Derived 类特有的托管资源
  56.                 // ...
  57.             }
  58.             // 释放 Derived 类特有的非托管资源
  59.             // ...
  60.             base.Dispose(disposing);
  61.             
  62.             _disposed = true;
  63.         }
  64.     }
  65. }
复制代码
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册