.NET C#基礎(9):資源釋放 - 需要介入的資源管理

来源:https://www.cnblogs.com/HiroMuraki/archive/2023/09/11/17693661.html
-Advertisement-
Play Games

1. 什麼是IDisposable? IDisposable介面是一個用於約定可進行釋放資源操作的介面,一個類實現該介面則意味著可以使用介面約定的方法Dispose來釋放資源。其定義如下: public interface IDisposable { void Dispose(); } 上述描述中可 ...


1. 什麼是IDisposable?

  IDisposable介面是一個用於約定可進行釋放資源操作的介面,一個類實現該介面則意味著可以使用介面約定的方法Dispose來釋放資源。其定義如下:
public interface IDisposable
{
    void Dispose();
}
  上述描述中可能存在兩個問題:   1. 什麼是“資源”?   2. C#是一個運行在含有垃圾回收(GC)平臺上的語言,為什麼還需要手動釋放資源?

1.1 資源

  資源包括托管資源和非托管資源,具體來說,它可以是一個普通的類實例,是一個資料庫連接,是一個文件指針,或者是一個視窗的句柄等等。不太準確地說,你可以理解為就是程式運行時用到的各種東西。

1.2 為什麼要手動釋放資源

  對於托管資源,通常來說由於CLR的GC的幫助,可以自動釋放回收而無需程式員手動管理。然而,由於C#允許使用非托管資源,這些非托管資源不受GC的控制,無法自動釋放回收,因此對於這類資源,就要程式員進行手動管理。另一方面,有些資源雖然是托管資源,但是實際包裝了一個非托管資源,並實現了IDispose介面,同樣的,對於這類資源,最好也手動管理。 (ps:CLR,Common Language Runtime,即C#編譯後的IL代碼的運行平臺) (ps:GC,Garbage Collection,垃圾回收,即一種用於自動回收資源的機制)   如果你寫過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進行管理) (ps:using本質上是try...finally的語法糖,所以即便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();
    }
}

(ps:你或許會覺得終結器很像C++的析構函數,無論是聲明方式還是作用(釋放資源)上,但是終結器和析構函數有本質上差別,但這裡不展開討論)

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 HandleResource : UnmanagedResource
{
    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 方法


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文深入探討了Go語言中的代碼包和包引入機制,從基礎概念到高級應用一一剖析。文章詳細講解瞭如何創建、組織和管理代碼包,以及包引入的多種使用場景和最佳實踐。通過閱讀本文,開發者將獲得全面而深入的理解,進一步提升Go開發的效率和質量。 關註公眾號【TechLeadCloud】,分享互聯網架構、雲服務技術 ...
  • 在併發編程中我們為啥一般選用創建多個線程去處理任務而不是創建多個進程呢?這是因為線程之間切換的開銷小,適用於一些要求同時進行並且又要共用某些變數的併發操作。而進程則具有獨立的虛擬地址空間,每個進程都有自己獨立的代碼和數據空間,程式之間的切換會有較大的開銷。 ...
  • 日誌是應用程式的重要組成部分。無論是服務端程式還是客戶端程式都需要日誌做為錯誤輸出或者業務記錄。在這篇文章中,我們結合log4rs聊聊rust 程式中如何使用日誌。 ...
  • 本章筆者將介紹一種通過Metasploit生成ShellCode並將其註入到特定PE文件內的Shell植入技術。該技術能夠劫持原始PE文件的入口地址,在PE程式運行之前執行ShellCode反彈,執行後掛入後臺並繼續運行原始程式,實現了一種隱蔽的Shell訪問。而我把這種技術叫做位元組註入反彈。位元組註... ...
  • 1.網關介紹 如果沒有網關,難道不行嗎?功能上是可以的,我們直接調用提供的介面就可以了。那為什麼還需要網關? 因為網關的作用不僅僅是轉發請求而已。我們可以試想一下,如果需要做一個請求認證功能,我們可以接入到 API 服務中。但是倘若後續又有服務需要接入,我們又需要重覆接入。這樣我們不僅代碼要重覆編寫 ...
  • 開心一刻 昨晚和一個朋友聊天 我:處對象嗎,咱倆試試? 朋友:我有對象 我:我不信,有對象不公開? 朋友:不好公開,我當的小三 問題背景 程式在生產環境穩定的跑著 直到有一天,公司執行組件漏洞掃描,有漏洞的 jar 要進行升級修複 然後我就按著掃描報告將有漏洞的 jar 修複到指定的版本 自己在開發 ...
  • Unity 性能優化Shader分析處理函數:ShaderUtil.GetShaderGlobalKeywords用法 點擊封面跳轉下載頁面 簡介 Unity 性能優化Shader分析處理函數:ShaderUtil.GetShaderGlobalKeywords用法 在Unity開發中,性能優化是一 ...
  • try catch使用場景: 1. 一般線上程,委托中使用, 線上程與委托中使用是因為,如果線程和委托中出現異常在程式外部是捕獲不到的,需要在內部做單獨處理。 2. 程式的外層使用,比如程式的入口處加一個全局異常捕獲,這樣整個程式發生的異常都可以捕獲到。 3. 在事件或者主體方法中使用,一些小的公共 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...