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

.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<T>),不需要手动释放,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;
        }
    }
}
posted @ 2026-03-25 17:12  误会馋  阅读(13)  评论(0)    收藏  举报