首页 > 编程语言 >.NET C#基础(9):资源释放 - 需要介入的资源管理

.NET C#基础(9):资源释放 - 需要介入的资源管理

时间:2023-09-11 15:56:48浏览次数:46  
标签:释放 调用 C# 终结 Dispose UnmanagedResource NET 资源管理 资源

1. 什么是IDisposable?

  IDisposable接口是一个用于约定可进行释放资源操作的接口,一个类实现该接口则意味着可以使用接口约定的方法Dispose来释放资源。其定义如下:
public interface IDisposable
{
    void Dispose();
}
  上述描述中可能存在两个问题:   1. 什么是“资源”?   2. C#是一个运行在含有垃圾回收(GC)平台上的语言,为什么还需要手动释放资源?

1.1 资源

  资源包括托管资源和非托管资源,具体来说,它可以是一个普通的类实例,是一个数据库连接,是一个文件指针,或者是一个窗口的句柄等等。不太准确地说,你可以理解为就是程序运行时用到的各种东西。

1.2 为什么要手动释放资源

  对于托管资源,通常来说由于有GC的帮助,可以自动释放回收而无需程序员手动管理。然而,由于C#允许使用非托管资源,这些非托管资源不受GC的控制,无法自动释放回收,因此对于这类资源,就要程序员进行手动管理。 (ps:CLR,Common Language Runtime,即C#编译后的IL代码的运行平台)   如果你写过C++,这就相当于应该在实例销毁时释放掉成“new”出来的分配到堆上的资源,否则资源将一直保留在内存中无法释放,导致内存泄漏等一系列问题。   在C++中,通常将资源释放的操作放置在类的析构函数中,但C#并没有析构函数这一概念,因此,C#使用IDisposable接口来对资源释放做出约定——当程序员看到一个类实现IDisposable接口时,就应该想到在使用完该类的实例后就应该调用其Dispose方法来及时释放资源。   对于实现了IDispose接口的类,在C#中你通常可以采用如下方式来释放资源:   1:try...finally
UnmanagedResource resource = /* ... */;

try
{
    // 各种操作
}
finally
{
    resource.Dispose();
}

(注:在finally中释放是为了确保即便运行时出错也可以顺利释放资源)

  2:using

using (UnmanagedResource resource = /* ... */)
{
    // 离开using的作用域后会自动调用resource的Dispose方法
}

// 或者如果不需要额外控制作用域的简写
using UnmanagedResource resource = /* ... */;
(ps:实际上,哪怕不实现IDisposable接口,只要类实现了public void Dispose()方法都可以使用using进行管理)

 

2. 如何实现IDisposable

2.1 不太完美的基本实现

  你可能还会认为IDisposable很容易实现,毕竟它只有一个方法需要实现,并且看上去只要在方法里释放掉需要释放的资源即可:
class UnmanagedResource : IDisposable
{
    public void Dispose()
    {
        // 释放需要释放的资源
    }    
}
  通常来说这样做也不会有什么大问题,然而,有几个问题需要考虑。接下来将逐步阐述问题并给出解决方案。

2.2 如果使用者忘记了调用Dispose方法释放资源

  尽管程序员都应该足够细心来保证他们对那些实现了Disposable接口的类的实例调用Dispose方法,但是,出于各种原因,或许是他是一名新手,或许他受到老板的催促,或许他昨天没睡好等等,这些都可能导致他没有仔细检查自己的代码。
永远不要假设你的代码会被一直正确地使用,总得留下些兜底的东西,提高健壮性——把你的用户当做一个做着布朗运动的白痴,哪怕他可能是个经验丰富的程序员,甚至你自己。
  对于这样的问题,最自然的想法自然是交给GC来完成——如果程序员忘记了调用Dispose方法释放资源,就留着让GC来调用释放。还好,C#允许你让GC来帮助你调用一些方法——通过终结器。   关于终结器的主题会是一个比较复杂的主题,因此在这里不展开讨论,将更多的细节留给其他主题。就本文而言,暂时只需要知道终结器的声明方法以及GC会在“某一时刻”自动调用终结器即可。(你或许想问这个“某一时刻”是什么时候,这实际上是需要交给复杂主题来讨论的话题)   声明一个终结器类似于声明一个构造方法,但是需要在方法的类名前添加一个~。如下:
class UnmanagedResource : IDisposable
{
    // UnmanagedResource的终结器
    ~UnmanagedResource()
    {
        // 一些操作
    }
}
    关于终结器,下面是一些你需要知道的:     1:一个类中只能定义一个终结器,且终结器不能有任何访问修饰符(即不能添加public/private/protected/internal)     2:永远不要手动调用终结器(实际上你也无法这么做)   由于GC会在某一个时刻自动调用终结器,因此如果在终结器中调用Dispose方法,即使有粗心的程序员忘记了手动释放资源,GC也会在某一时刻来帮他们兜底。如下:
class UnmanagedResource : IDisposable
{
    public void Dispose()
    {
        // 释放需要释放的资源
    }  

    ~UnmanagedResource()
    {
        // 终结器调用Dispose释放资源
        Dispose();
    }
}

2.3 手动调用了Dispose后,终结器再次调用Dispose

  当你手动调用了Dispose方法后,并不表示你就告诉了GC不要再调用它的终结器,实际上,在你调用Dispose方法后,GC还是会在某一时刻调用终结器,而由于我们在终结器里调用了Dispose方法,这会导致Dispose方法再次被调用——Double Free!   当然,要解决这一问题非常简单,只需要用一个字段来表明资源是否被释放,并在Dispose方法里检查这个字段的值,一旦发现已经释放则过就立刻返回。如下:
class UnmanagedResource : IDisposable
{
    public void Dispose()
    {
        // 如果已经释放过就立刻返回
        if (_disposed)
        {
            return;
        }
   
        // 释放需要释放的资源
        
        // 标记已释放
        _disposed = true;
    }  

    ~UnmanagedResource()
    {
        Dispose();
    }

    // 用于标记是否已经释放的字段
    private bool _disposed;
}
  这样可以解决资源被重复释放的问题,但是这还是无法阻止GC调用终结器。当然你或许会认为让GC调用终结器没什么问题,毕竟我们保证了Dispose重复调用是安全的。不过,要知道终结器是会影响性能的,因此为了性能考虑,我们还是希望在Dispose方法调用后阻止终结器的执行(毕竟这时候已经不需要GC兜底了)。而要实现这一目标十分简单,只需要在Dipose方法中使用GC.SuppressFinalize(this)告诉GC不要调用终结器即可。如下:
class UnmanagedResource : IDisposable
{
    public void Dispose()
    {
        if (_disposed)
        {
            return;
        }
   
        // 释放需要释放的资源

        _disposed = true;
       
        // 告诉GC不要调用当前实例(this)的终结器
        GC.SuppressFinalize(this);
    }  

    ~UnmanagedResource()
    {
        Dispose();
    }

    private bool _disposed;
}
  这样,如果调用了Dispose方法,就会“抑制”GC对终结器的调用;而让终结器调用Dispose也不会产生什么问题。

2.4 不是任何时候都需要释放所有资源

  考虑一个比较复杂的类:
class UnmanagedResource : IDisposable
{
   // 其他代码
   
    private FileStream _fileStream;
}
  上述例子中,FileStream是一个实现了IDisposable的类,也就是说,FileStream也需要进行释放。UnmanagedResource不仅要释放自己的非托管资源,还要释放FileStream。你或许认为只需要在UnmanagedResourceDispose方法中调用一下FileStreamDispose方法就行。如下:
class UnmanagedResource : IDisposable
{
    // 其它代码    
    
    public void Dispose()
    {
        // 其他代码

        _fileStream.Dispose();

        // 其它代码
    }

    private FileStream _fileStream;
}
  咋一看没什么问题,但是考虑一下,如果UnmanagedResourceDispose方法是由终结器调用的会发生什么?   提示:终结器的调用是无序的。   是的,很可能FileStream的终结器先被调用了,执行过了其Dispose方法释放资源,随后UnmanagedResource的终结器调用Dispose方法时会再次调用FileStreamDispose方法——Double Free, Again。   因此,如果Dispose方法是由终结器调用的,就不应该手动释放那些本身就实现了终结器的托管资源——这些资源的终结器很可能先被执行。仅当手动调用Dispose方法时才手动释放那些实现了终结器的托管资源。   我们可以使用一个带参数的Dispose方法,用一个参数来指示Dispose是否释放托管资源。稍作调整,实现如下:
class UnmanagedResource : IDisposable
{
    // 其它代码
    private void Dispose(bool disposing)
    {
        // 其他代码
       
        if (disposing)
        {
            // 释放托管资源
            _fileStream.Dispose();
        }
       
        // 释放非托管资源

        // 其它代码
    }
}
  上述代码声明了一个接受disposing参数的Dispose(bool disposing)方法,当disposingtrue时,同时释放托管资源和非托管资源;当disposingfalse时,仅释放托管资源。另外,为了不公开不必要的接口,将其声明为private。   接下来,只需要在Dispose方法和终结器中按需调用Dispose(bool disposing)方法即可。
class UnmanagedResource : IDisposable
{
    // 其它代码

    public void Dispose()
    {
        // disposing=true,手动释放托管资源
        Dispose(true);
        GC.SuppressFinalize(this);
    }    
    
    ~UnmanagedResource()
    {
        // disposing=false,不释放托管资源,交给终结器释放
        Dispose(false);
    }
    
    private void Dispose(bool disposing)
    {
        if (_disposed)
        {
            return;
        }
   
        if (disposing)
        {
            // 释放托管资源
        }

        // 释放非托管资源

        _disposed = true;
    }
}

2.5 考虑一下子类的资源释放

  考虑一下如果有UnmanagedResource的子类:
class HandleResource : UnmanagedResource
{
    private HandlePtr _handlePtr;
}
  HandleResource有自己的资源HandlePtr,显然如果只是简单继承UnmanagedResource的话,UnmanagedResourceDispose方法并不能释放HandleResourceHandlePtr。   那么怎么办呢?使用多态,将UnmanagedResourceDispose方法声明为virtual并在HandleResource里覆写;或者在HandleResource里使用new重新实现Dispose似乎都可以:
// 使用多态
class UnmanagedResource : IDisposable
{
    public virtual void Dispose() { /* ... */}
}
class HandleResource : UnmanagedResource
{
    public override void Dispose() { /* ... */}
}


// 重新实现
class UnmanagedResource : IDisposable
{
    public void Dispose() { /* ... */}
}
class HandleResource : UnmanagedResource
{
    public new void Dispose() { /* ... */}
}
  这两种方法似乎都可行,但是一个很大的问题是,你还得对HandleResource重复做那些在它的父类UnmanagedResource做过的事——解决重复释放、定义终结器以及区分对待托管和非托管资源。

  这太不“继承了”——显然,有更好的实现方法。

  答案是:将UnmanagedResource的的Dispose(bool disposing)方法访问权限更改为protected,并修饰为virtual,以让子类访问/覆盖:
class UnmanagedResource : IDisposable
{
    protected virtual void Dispose(bool disposing) { /* ... */ }
}
  这样,子类可以通过覆写Dispose(bool disposing)来实现自己想要的释放功能:
class UnmanagedResource : IDisposable
{
    protected override void Dispose(bool disposing)
    {
        // 其他代码
        
        base.Dispose(disposing);
    }
}
(ps:建议先释放子类资源,再释放父类资源)   由于Dispose(bool disposing)是虚方法,因此父类UnmanagedResource的终结器和Dispose方法中对Dispose(bool disposing)的调用会受多态的影响,调用到正确的释放方法,故子类可以不必再做那些重复工作。

 

3. 总结

3.1 代码总览

class UnmanagedResource : IDisposable
{
    // 对IDisposable接口的实现
    public void Dispose()
    {
        // 调用Dispose(true),同时释放托管资源与非托管资源
        Dispose(true);
        // 让GC不要调用终结器
        GC.SuppressFinalize(this);
    }    
    
    // UnmanagedResource的终结器
    ~UnmanagedResource()
    {
        // 调用Dispose(false),仅释放非托管资源,托管资源交给GC处理
        Dispose(false);
    }
    
    // 释放非托管资源,并可以选择性释放托管资源,且可以让子类覆写的Dispose(bool disposing)方法
    protected virtual void Dispose(bool disposing)
    {
        // 防止重复释放
        if (_disposed)
        {
            return;
        }
       
        // disposing指示是否是否托管资源
        if (disposing)
        {
            // 释放托管资源
        }

        // 释放非托管资源
        
        // 标记已释放
        _disposed = true;
    }
}

 

参考资料/更多资料:

【1】:IDisposable 接口

【2】:实现 Dispose 方法

标签:释放,调用,C#,终结,Dispose,UnmanagedResource,NET,资源管理,资源
From: https://www.cnblogs.com/HiroMuraki/p/17693661.html

相关文章

  • nopi 2.6.1 读word docx,写Excel xsls 源代码例子
    ///<summary>///获取.docx文件内容,使用NPOI.XWPF插件解析///</summary>///<paramname="strFilePath">文件路径</param>///<returns></returns>publicstringGetDocxContent(stri......
  • .NET Framework 4.7.2下 Hangfire 的集成(转载)
    原文地址:.NETFramework4.7.2下Hangfire的集成-SamXiao-博客园(cnblogs.com).NETFramework4.7.2下Hangfire的集成  参考资料:开源的.NET定时任务组件Hangfire解析:https://www.cnblogs.com/pengze0902/p/6583119.html.NetCore简单的Hangfire部署Demo:https://......
  • 浅谈Apache Shiro CVE-2023-22602
    一、漏洞描述  ApacheShiro是一个可执行身份验证、授权、加密和会话管理的Java安全框架。  由于1.11.0及之前版本的Shiro只兼容Spring的ant-style路径匹配模式(patternmatching),且2.6及之后版本的SpringBoot将SpringMVC处理请求的路径匹配模式从AntPathMat......
  • H3C服务器使用hREST工具命令安装系统
    H3C服务器hREST命令行工具使用Python语言开发,主要基于HTTPs协议和Redfish、RESTful和IPMI接口协议,是一款便于用户管理服务器的客户端工具。用户可通过本工具的查询、设置等命令对服务器进行管理。hREST1.23工具下载地址:http://www.h3c.com/cn/BizPortal/DownLoadAccesso......
  • 数据库数据恢复-Oracle数据库误执行truncate table的数据恢复案例
    Oracle数据库故障&分析:北京某单位Oracle11gR2数据库误执行truncate table CM_CHECK_ITEM_HIS,表数据丢失,查询该表时报错。数据库备份无法使用,表数据无法查询。Oracle数据库Truncate数据的机理:执行Truncate命令后,ORACLE数据库会在数据字典和Segment Header中更新表的Data O......
  • 无涯教程-JavaScript - MDURATION函数
    描述MDURATION函数返回假定面值为$100的有价证券的经修改Macaulay期限。语法MDURATION(settlement,maturity,coupon,yld,frequency,[basis])争论Argument描述Required/OptionalSettlement证券的结算日期。证券结算日期是指在发行日期之后将证券交易给买方的日......
  • 【心得】TP6,使用phpspreadsheet库进行EXCEL的数据导入导出
    在日常开发中,我们会遇到大批量的数据导出以及导入,之前的PHP旧库现在已经停更了,如下提示:composerrequirephpoffice/phpexcelPackagephpoffice/phpexcelisabandoned,youshouldavoidusingit.Usephpoffice/phpspreadsheetinstead.phpoffice/phpexcel包已废弃,应避免......
  • Socks5代理IP在跨境电商与网络游戏中的网络安全应用
    在数字化时代,跨境电商和网络游戏已成为全球网络世界中的两大热门领域。然而,这两者都面临着相似的网络安全挑战,需要高效的网络代理来解决。本文将讨论Socks5代理IP在跨境电商和网络游戏中的关键作用,以及如何通过这一技术增强网络安全。1.了解Socks5代理IPSocks5代理是一种网络协议,......
  • linux服务器上的nginx服务、mysql服务和docker里面的php服务配合使用
    之前有个老项目是nginx1.22.0+mysql5.7+php5.6的环境在跑,也就是常说的lnmp环境。但是最近出了一个新的需求,这台服务器上要跑一个php7.3的项目,mysql5.7还可以用,nginx1.22.0也可以用,主要是php的环境要升级到7.3,那么方案应该怎么实施呢,大概有下面几个思路:1、再独立安装一个php7.3的......
  • 开源的网络诊断工具OpenTrace
    简介OpenTrace 是使用.NET6和Eto框架开发的NextTrace的跨平台GUI界面,带来您熟悉但更强大的用户体验。软件特点开源跟踪工具:这是一个开源项目,旨在提供一个方便、有效的路由追踪工具。跨平台:支持Windows和Linux操作系统,满足多种环境下的使用需求。集成NextTrace:......