C#中的Dispose模式

2023-11-11

声明

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

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

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

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。

参考资料

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C#中的Dispose模式 的相关文章

随机推荐

  • Python time strftime() 方法

    描述 Python time strftime 函数用于格式化时间 返回以可读字符串表示的当地时间 格式由参数 format 决定 语法 strftime 方法语法 time strftime format t 参数 format 格式字符
  • SpringBoot项目配置跨域报错When allowCredentials is true, allowedOrigins cannot contain the special value

    项目配置允许跨域访问后 报When allowCredentials is true allowedOrigins cannot contain the special value错误 解决办法找到配置允许跨域访问配置的地方 找到如下配置
  • 二叉搜索树 BST

    文章目录 一 判断 BST 的合法性 Q98 迭代写法见提交记录 使用stack 二 在 BST 中搜索一个数 Q700 三 在 BST 中插入一个数 Q701 四 在 BST 中删除一个数 Q450 最后总结 原文 https mp we
  • A优秀的springcloud项目部署及代码。

    在线演示地址 http 114 115 178 160 8082 index 演示服务器内存宽带比较小 可能导致演示系统比较卡 第一次加载可能要20秒 为防止人为恶意删除我演示系统数据库演示系统将禁止修改数据库 敬请谅解 在这个社会上总有一
  • 【Java】jdk5.0、jdk7、jdk8、jdk11、jdk17新特性

    文章目录 前言 一 jdk5 0新特性 二 jdk7新特性 三 jdk8新特性 四 jdk11新特性 五 jdk17新特性 前言 了解jdk新特性 一 jdk5 0新特性 泛型 允许类型或方法对各种类型的对象进行操作 同时提供编译时类型安全
  • 华为OD机试-高性能AI处理器-2022Q4 A卷-Py/Java/JS

    某公司研发了一款高性能AI处理器 每台物理设备具备8颗AI处理器 编号分别为0 1 2 3 4 5 6 7 编号0 3的处理器处于同一个链路中 编号4 7的处理器处于另外一个链路中 不同链路中的处理器不能通信 现给定服务器可用的处理器编号数
  • Opencascade之STL可视化与选取渲染风格

    在Opencascade中 STL模型可以的可视化有两种方式 一 STL模型加载为Shape对象 再可视化 1 1 STL模型加载为Shape对象 TopoDS Shape aShape try StlAPI Reader reader r
  • LeetCode两个数组的交集

    两个数组的交集 给定两个数组 nums1 和 nums2 返回 它们的交集 输出结果中的每个元素一定是 唯一 的 我们可以 不考虑输出结果的顺序 输入 nums1 1 2 2 1 nums2 2 2 输出 2 输入 nums1 4 9 5
  • Maven —— Plugin execution not covered by lifecycle configuration 错误

    转载自 https blog csdn net lmxmimihuhu article details 34436205 一 错误描述 Eclipse 导入已存在的Maven 后 pom xml 文件的execution 节点报错 错误位置
  • 网络安全的方向好就业吗

    一些网络安全专业方向的同学难免会有疑问 这个方向好就业吗 我能做些什么呢 今天就业老师就给大家解答一下相关的问题 网络安全是目前为止比较容易就业的一个方向 现今我国是被黑客攻击 信息丢失最严重的的国家之一 因此在市场需求和政策引导的共同推动
  • 微信小程序-0.11.122100版本更新问题

    官方更新了122100版本 一共有90几处改动 这里先不一一列举了 一 redirectTo和navigateTo不能再跳转到带有tab选项卡的页面 小程序新增了一个接口wx switchTab 这个接口是专门用来跳转到带有tabbar的页
  • 1.平台介绍:FISCO BCOS 区块链

    引言 区块链技术作为一种分布式 安全可信的数据记录和交互方式 正逐渐在各行各业展现出巨大潜力 然而 公共区块链的隐私性和性能限制使得企业更倾向于采用联盟链或私有链解决方案 FISCO BCOS Blockchain Open Consort
  • wildfly(JBOSS)启动报错

    报错内容 15 07 50 724 ERROR org jboss as controller management operation Controller Boot Thread WFLYCTL0013 Operation add fa
  • Delphi 判断fast report 是否存在未定义对象

    Assigned fastreport 对象 Boolean 返回 sql Fields i FieldName 获取列名 sql select from table tmp ADOQuery DLLMan Exe server Query
  • ansys选择一个面上所有节点_ANSYS选中无序关键点、节点并得到其编号

    看样子选中无序关键点 节点并得到其编号不可能的 在ANSYS中 有时我们需要采用APDL命令流选中一堆无序的关键点或节点 也就是这堆关键点或节点的编号完全是任意不连续的 因此无法采用根据编号来选中这堆无序的关键点或节点 而且假如最不利情况下
  • dockerfile创建lnmp镜像

    目录 一 创建lnmp的相关镜像 1 1 dockerfile创建php7 2 16镜像 1 2 dockerfile创建nginx 1 15 7镜像 1 3 mysql镜像是直接在docker仓库上pull 二 通过dockerpose
  • 串口服务器网页进不去怎么办,路由器登录入口进不去怎么办?

    问 路由器登录入口进不去怎么办 答 如果在设置路由器的时候 进不去路由器的登录入口 无法对路由器进行设置 这多半是用户自己操作有误导致的 也可能是路由器或者其它客观原因引起的 具体的解决办法如下 温馨提示 1 如果是用手机设置路由器时 手机
  • clang 01.clang简介

    文章目录 前言 1 Clang的工作流程 前言 Clang的官方网站是 http clang llvm org 它被认为是C家族的LLVM前端 Clang可能指代三种不同的实体 前端 由Clang程序库实现 编译器驱动器 由Clang命令和
  • 本机如何传文件到VMware 中

    本机传文件到VMware 中可以使用2种方法 1 安装tools 直接拖拽过去 2 实现文件共享 在VMware中没有安装解压文件的应用时 使用tools会不再适用 这时可以选择共享文件夹的方式 直接在本机解压文件 共享文件夹到VMware
  • C#中的Dispose模式

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