芮梦月 发表于 昨天 19:09

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

目录

[*].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 的方法。
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

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

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

主动调用 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;
      }
    }
}
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: net C# 如何理解和实现 Dispose 方法