C#中的Dispose模式

声明

本文中的内容属于个人总结整理而来,个人水平有限,对于部分细节难免有理解错误及遗漏之处,如果您在阅读过程中有所发现,希望您能指正,同时文章中的部分内容也参考了其它大神的文章,如果文章中的内容侵犯了您的权益,表示非常歉意,请您指出,我将尽快修改。

如果文章中的内容侵犯了您的权益,表示非常歉意,请您指出,我将尽快修改。

如果您进行转载,请标明出处。

C#中的Dispose模式(http://www.liyubin.com/articles/2020/11/12/1605171494657.html)

简介

在C#中的每一个类型都可以看做是一种资源,这些资源可以分为两类:

  • 托管资源:由CLR管理分配和释放的资源,即从CLR里new出来的对象。
  • 非托管资源:不受CLR管理的对象,如Windows的窗口,文件句柄、数据库连接、套接字、COM对象等。

对于托管资源来说,由于受到CLR的管理,我们不需要关心资源释放的问题,完全可以依赖于垃圾回收器来进行内存的管理,但是对于非托管资源来说,由于其不受CLR管理,使用完毕后,就需要显式释放这些资源。

对于非托管资源,则应该执行以下操作:

  • 实现Dispose模式

    这要求你提供 IDisposable.Dispose 实现以启用非托管资源释放,当不再需要此对象(或其使用的资源)时,调用者可以通过调用Dispose方法释放非托管资源

  • 调用者忘记调用Dispose方法的情况下,需要提供一种方法来释放非托管多资源

    • 使用安全句柄包装非托管资源
    • 重写 Object.Finalize 方法

本方将重点总结一下Dispose模式的实现方式及注意事项,同时会结合《编写高质量代码改善C#程序的157个建议》一书中的内容进行一下总结。

IDisposable接口

C#提供了标准的接口IDisposable,通过实现接口中的Dispose方法来实现非托管资源的释放。

public interface IDisposable
{
    void Dispose();
}

通过实现IDisposable接口中的Dispose方法,可以显式的释放非托管资源,同时也在提醒调用者此类型的资源需要通过Dispose方法的调用来显式释放资源

标准的Dispose模式的实现

using System;
public class BaseClass : IDisposable
{
    // 添加标识,用于标识此资源是否已经回收过了
    private bool _disposed = false;

    //防止调用者未显式调用Dispose方法
    ~BaseClass()
    {
        Dispose(false);
    } 
    
    // 实现IDisposable中的Dispose方法
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed)
        {
            return;
        }

        if (disposing)
        {
            // TODO: 释放托管资源
        }

        // TODO: 释放非托管资源

        _disposed = true;
    }

    public void PMethod()
    {
        if(__disposed)
        {
            throw new ObjectDisposedException("it has been disposed");
        }else
        {
            ...
        }
    }
}

标准的Dispose模式剖析

以下内容主要来自《编写高质量代码改善C#程序的157个建议》一书

  • 如果类型需要显式的释放资源,那么一定要继承IDispose接口

    在.NET中每次使用new操作符创建对象时,CLR都会为该对象在堆上分配内存。对于没有继承IDisposable接口的类型对象,垃圾回收器则会直接释放对象所占用的内存:而对于实现了Dispose模式的类型,每次创建对象的时候,CLR都会将该对象的一个指针放到终结列表中,垃圾回收器在回收该对象的内存前,首先将终结列表中的指针放到一个freachable队列中。同时,CLR还会分配专门的线程读取freachable队列,并调用对象的终结器,只有这个时候对象才会真正被识别为垃圾,并且在下一次进行垃圾回收时释放对象所占的内存。

    除此外继承IDispose接口为实现Using语法糖提供了便利,编译器会自动生成IDispose方法的IL代码:

    using(BaseClass bClass = new BaseClass())
    {
    
    }
    

    某种意义上来说上述的代码段相当于:

    BaseClass bClass;
    try
    { 
        bClass = new BaseClass();
    }finally{
        bClass.Dispose();
    }
    
  • 即使提供了显式的释放方法,也应该在终结器中提供隐式清理

    在标准的Dispose模式中,可以看到终结器(析构函数)的实现:

      ~BaseClass()
      {
          Dispose(false);
      } 
    

    为当前类型提供终结器的意义在于,我们不能奢望类型的调用者肯定会主动调用Dispose方法,基于终终结器(析构函数)会被垃圾回收器调用这个特点下,它将用作资源释放的补救措施。

    对于实现了Dispose模式的类型对象,起码要经过两次垃圾回收才能真正地被回收掉,因为垃圾回收机制会安排CLR调用终结器。基于这个特点,如果我们的类型提供了显式释放的方法来减少一次垃圾回收,同时也可以在终结器中提供隐式清理,以避免调用者忘记调用该方法而带来的资源泄漏。

    如果调用者已经调用Dispose方法进行了显式地资源释放,那么,隐式资源释放(就是终结器)就没有必要再运行了。FCL中的类型GC提供了静态方法SuppressFinalize来通知垃圾回收器这一点。

    public void Dispose()
    {
        Dispose(true);//进行正常的资源回收
        GC.SuppressFinalize(this);//通知垃圾回收器不再需要调用终结器(析构函数)
    }
    
  • Dispose方法应允许被多次调用

    在类型的内部通过维护一下私有的变量_disposed,来标识对象是否被释放过,以此来保证Dispose方法被多次调用而不抛出异常。

    private bool _disposed = false;
    

    在实际清理代码的方法中,加入一下判断,如果类型已经被清理过一次则清理工作将不再进行

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed)//如果已经被清理过,将不再执行
        {
            return;
        }
        ......
    }
    

    需要注意的是:对象被调用过Dispose方法,并不表示该对象已经被垃圾回收器回收内存,被置为null,已经不存在了。事实上该对象的引用可能还在,由于对象被Dispose过,部分资源已经被释放,对象当前状态已经不正常了,那些在这种情况下如果调用对象的公开的方法,应该会抛出一个异常。

    public void PMethod()
    {
        if(__disposed)
        {
            throw new ObjectDisposedException("it has been disposed");
        }else
        {
            ...
        }
    }
    
  • 在Dispose模式中应该提取一个受保护的虚方法

    在标准的Dispose模式的实现可以看到IDisposable接口的Dispose方法并没有做实际的清理工作,它其实是调用了带bool参数且受保护的的虚方法:

    protected virtual void Dispose(bool disposing)
    {
        ...
    }
    

    之所以提供这样一个受保护的虚方法,是因为考虑了这个类型会被其他类型继承的情况。如果类型存在一个子类,子类也许会实现自己的Dispose模式。受保护的虚方法用来提醒子类:必须在自己的清理方法时注意到父类的清理工作,即子类需要在自己的释放方法中调用base.Dispose方法。

    using Microsoft.Win32.SafeHandles;
    using System;
    using System.Runtime.InteropServices;
    
    class DerivedClass : BaseClass
    {
        // To detect redundant calls
        private bool _disposed = false;
    
        // Instantiate a SafeHandle instance.
        private SafeHandle _safeHandle = new SafeFileHandle(IntPtr.Zero, true);
    
        // Protected implementation of Dispose pattern.
        protected override void Dispose(bool disposing)
        {
            if (_disposed)
            {
                return;
            }
    
            if (disposing)
            {
            // Dispose managed state (managed objects).
                _safeHandle?.Dispose();
            }
    
            _disposed = true;
    
            // 调用父类的Dispose
            base.Dispose(disposing);
        }
    }
    

    如果不为类提供这个受保护的虚方法,很有可能让开发者设计子类的时候忽略掉父类的清理工作。所以要在类型的Dispose模式中提供一个受保护的虚方法

  • 在Dispose模式中应该区别对待托管资源和非托管资源

    真正资源释放代码的那个虚方法是带一个bool参数的,带这个参数,是因为我们在资源释放时要区别对待托管资源和非托管资源。

    public void Dispose()
    {
        //必须为true
        Dispose(true);
        ...
    }
    

    这时候代码要同时处理托管资源和非托管资源。

    在终结器(析构函数)中调用的是false

    ~BaseClass()
    {
        //必须为false
        Dispose(false);
    }
    

    这表明隐式清理时,只要处理非托管资源就可以了。

    为什么要区别对待托管资源和非托管资源呢?

    在这个问题前,我们首先要弄明白:托管资源需要手工清理吗?不妨将C#中的类型分为两类,一类继承了IDisposable接口,一类则没有继承。前者,暂时称为非普通类型,后者称为普通类型。非普通类型因为包含非托管资源,所以它需要继承IDisposable接口,但是,这里包含非托管资源的类型本身,它是一个托管资源。所以,托管资源中的普通类型不需要手动清理,而非普通类型是需要手工清理的(即调用Dispose方法)。

    Dispose模式设计的思路是:如果调用者显式调用了Dispose方法,那么类型就应该按部就班地将自己的资源全部释放。如果调用者忘记调用Dispose方法,那么类型就假设自己的所有托管资源(哪怕是那些非普通类型)会全部都交给垃圾回收器回收,所以不进行手工清理。所以在Dispose方法中,虚方法传入参数true,在终结器中,虚方法传入参数false。

参考资料

  • 6
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C#Dispose是一种用于显式释放资源的模式。通过实现IDisposable接口Dispose方法,可以显式释放非托管资源,并提醒调用者需要通过调用Dispose方法来释放资源。Dispose模式的标准实现包括以下几个步骤: 1. 在实现类添加一个标识,用于标识资源是否已经被释放过。 2. 实现类需要通过析构函数来调用Dispose方法,以确保即使调用者忘记调用Dispose,资源也能被释放。 3. 实现IDisposable接口Dispose方法,用于释放资源。在该方法,需要判断资源是否已经被释放过,如果已经释放,则直接返回;如果还未释放,则释放托管资源和非托管资源,并将标识置为已释放。 4. 如果实现了Dispose方法,需要在该方法调用GC.SuppressFinalize(this),以告诉垃圾回收器不再调用析构函数。 5. 在使用资源的其他方法,可以通过判断标识的值来判断资源是否已经被释放,以避免使用已经释放的资源。 总之,Dispose模式是一种在C#用于显式释放资源的标准模式,通过实现IDisposable接口Dispose方法,可以确保资源的正确释放。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [C#Dispose模式](https://blog.csdn.net/mingtianqingtian/article/details/109649759)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值