主頁 > .NET開發 > 《Effective C#》筆記(2) - .NET的資源管理

《Effective C#》筆記(2) - .NET的資源管理

2021-01-31 05:59:34 .NET開發

理解并善用.NET的資源管理機制

.NET環境會提供垃圾回收器(GC)來幫助控制托管記憶體,這使得開發者無須擔心記憶體泄漏等記憶體管理問題,盡管如此,但如果開發者能夠把自己應該執行的那些清理作業做好,那么垃圾回收器會表現得更為出色,非托管的資源是需要由開發者控制的,例如資料庫連接、GDI+物件、IO等;此外,某些做法可能會令物件在記憶體中所待的時間比你預想的更長,這些都是需要我們去了解、避免的,

GC的檢測程序是從應用程式的根物件出發,把與該物件之間沒有通路相連的那些物件判定為不可達的物件,也就是說,凡是無法從應用程式中的活動物件(live object)出發而到達的那些物件都應該得到回收,應用程式如果不再使用某個物體,那么就不會繼續參考它,于是,GC就會發現這個物體是可以回收的,
垃圾回收器每次運行的時候,都會壓縮托管堆,以便把其中的活動物件安排在一起,使得空閑的記憶體能夠形成一塊連續的區域,

針對托管堆的記憶體管理作業完全是由垃圾回收器負責的,但是除此之外的其他資源則必須由開發者來管理,
有兩種機制可以控制非托管資源的生存期

  • 一種是finalizer/destructure(解構式)
  • 另一種是IDisposable介面,

在這兩種方式中,應該優先考慮通過IDisposable介面來更為順暢地將資源及時返還給系統,因為finalizer作為一種防護機制,雖然可以確保物件總是能夠把非托管資源釋放掉,但這種機制有一些缺陷

  • 首先,C#的finalizer執行得并不及時,當垃圾回收器把物件判定為垃圾之后,它會擇機呼叫該物件的finalizer,但開發者并不知道具體的時機,因此,finalizer只能保證由某個型別的物件所分配的非托管資源最終可以得到釋放,但并不保證這些資源能夠在確定的時間點上得到釋放,因此,設計與撰寫程式的時候,盡量不要創建finalizer,即便創建了,也不要過多地依賴于它的執行時機,

  • 另外,依賴finalizer還會降低程式的性能,因為垃圾回收器需要執行更多的作業才能終結這些物件,如果GC發現某個物件已經成為垃圾,但該物件還有finalizer需要運行,那么就無法立刻把它從記憶體中移走,而是要等呼叫完finalizer之后,才能將其移除,呼叫finalizer的那個執行緒并不是GC所在的執行緒,GC在每一個周期里面會把包含finalizier但是尚未執行的那些物件放在佇列中,以便安排其finalizer的運行作業,而不含finalizer的物件則會直接從記憶體中清理掉,等到下一個周期,GC才會把已經執行了finalizer的那些物件刪掉,

宣告欄位時,盡量直接為其設定初始值

類的建構式有時不止一個,如果某個成員變數的初始化在建構式進行,就會有忘記給某些成員變數設定初始值的可能性,為了徹底杜絕這種情況,無論是靜態變數還是實體變數,最好都在宣告的時候直接初始化,而不要等實作每個建構式的時候再去賦值,

表面上看,在建構式初始化和在宣告的時候直接初始化等效,但實際上如果選擇在宣告的時候直接初始化,編譯器會把由這些陳述句所生成的程式碼放在類的建構式之前,這些陳述句的執行時機比基類的建構式更早,它們會按照本類宣告相關變數的先后順序來執行,

但也并不是說,如何時候都優先在宣告的時候直接初始化,在下面三種情況下,宣告的時候直接初始化是不建議的,甚至會帶來問題:

  1. 把物件初始化為0或null,系統在執行開發者所撰寫的代碼之前,本身就會生成初始化邏輯,以便把相關的內容全都設定成0,這是通過底層CPU指令來做的,這些指令會把整塊記憶體全都設定成0,因此,你如果還要撰寫初始化陳述句,讓編譯器會添加相關指令,把那些記憶體再度清零,那就顯得多余了,

  2. 如果不同的建構式需要按照各自的方式來設定某個欄位的初始值,那么就不應該再在宣告的時候初始化了,因為它只適用于那些總是按相同方式來初始化的變數,
    就類似這樣的寫法:

public class MyClass
{
  private List<string> labels = new List<string>();
  
  public MyClass(int size)
  {
    labels = new List<string>(size);
  }
}

這會在構造類實體的程序中創建出兩個不同的List物件,而且先創建出來的那個List馬上就會被后創建的List取代,實際上等于是白創建了一次,這是因為欄位的初始化陳述句會先于建構式而執行,于是,程式在初始化labels欄位時,會根據其初始化陳述句的要求創建出一個List,然后,等到執行建構式時,又會根據其中的賦值陳述句創建出另一個List,并導致前一個List失效,
編譯器所生成的代碼相當于下面這樣:

public class MyClass
{
  private List<string> labels;
  
  public MyClass(int size)
  {
    labels = new List<string>();
    labels = new List<string>(size);
  }
}
  1. 如果初始化變數的程序中有可能出現例外,那么就不應該使用初始化陳述句,而是應該把這部分邏輯移動到建構式里面,由于成員變數的初始化陳述句不能包裹在try-catch塊中,因此初始化的程序中一旦發生例外,就會傳播到物件之外,從而令開發者無法在類里面加以處理,應該把這種初始化代碼放在建構式中,以便通過適當的代碼將例外處理好,

用適當的方式初始化類中的靜態成員

通過靜態初始化陳述句或者靜態建構式都可以初始化類中的靜態成員,如果只需給靜態成員分配記憶體即可將其初始化,那么用一條簡單的初始化陳述句就足夠了,反之,若是必須通過復雜的邏輯才能完成初始化,則應考慮創建靜態建構式,
靜態初始化陳述句與實體欄位的初始化陳述句一樣,靜態欄位的初始化陳述句也會先于靜態建構式而執行,并且有可能比基類的靜態建構式執行得更早,如果靜態欄位的初始化作業比較復雜或是開銷比較大,那么可以考慮運用Lazy機制,將初始化作業推遲到首次訪問該欄位的時候再去執行,

靜態建構式是特殊的函式,會在初次訪問該類所定義的其他方法、變數或屬性之前執行,可以用來初始化靜態變數、實作單例(singleton)模式,或是執行其他一些必要的作業,以便使該類能夠正常運作,
當程式碼初次訪問應用程式空間(application space,也就是AppDomain)里面的某個型別之前,CLR會自動呼叫該類的靜態建構式,這種建構式每個類只能定義一個,而且不能帶有引數,

由于靜態建構式是由CLR自動呼叫的,因此必須謹慎處理其中的例外,如果例外跑到了靜態建構式外面,那么CLR就會拋出TypeInitialization-Exception以終止該程式,呼叫方如果想要捕獲這個例外,那么情況將會更加微妙,因為只要AppDomain還沒有卸載,這個型別就一直無法創建,也就是說,CLR根本就不會再次執行其靜態建構式,這導致該型別無法正確地加以初始化,并導致該類及其派生類的物件也無法獲得適當的定義,因此,不要令例外脫出靜態建構式的范圍,

不要創建無謂的物件

雖然垃圾回收器能夠有效地管理應用程式所使用的記憶體,但在堆上創建并銷毀物件仍需耗費一定的時間,因此應盡量避免過多地創建物件,也不要創建那些根本不用去重新構建的物件,此外,在函式中以區域變數的形式頻繁創建參考型別的物件也是不合適的,應該把這些變數提升為成員變數,或是考慮把最常用的那幾個實體設定成相關型別中的靜態物件,

絕對不要在建構式里面呼叫虛函式

這里有個建構式里面呼叫虛函式的demo,運行后列印出的結果是"VFunc in B",還是"VFunc in B1",還是"Msg from main"?答案是"VFunc in B1",

public class B
{
  protected B()
  {
    VFunc();
  }

  protected virtual void VFunc()
  {
    Console.WriteLine("VFunc in B");
  }
}

public class B1 : B
{
    private readonly string msg = "VFunc in B1";

    public B1(string msg)
    {
      this.msg = msg;
    }

    protected override void VFunc()
    {
      Console.WriteLine(msg);
    }

    public static void Init()
    {
      _ = new B1("Msg from main");
    }
}

為什么會這樣呢,這要從構建某個型別的首個實體時系統所執行的操作說起,步驟如下:

  1. 把存放靜態變數的空間清零,
  2. 執行靜態變數的初始化陳述句,
  3. 執行基類的靜態建構式,
  4. 執行本類的靜態建構式,
  5. 把存放實體變數的空間清零,
  6. 執行實體變數的初始化陳述句,
  7. 適當地執行基類的實體建構式,
  8. 執行本類的實體建構式,

所以會先初始化B1.msg,然后執行基類B的建構式,基類的建構式呼叫了一個定義在本類中但是為派生類所重寫的虛函式VFunc,于是程式在運行的時候呼叫的就是派生類的版本,因為物件的運行期型別是B1,而不是B,在C#語言中,系統會認為這個物件是一個可以正常使用的物件,因為程式在進入建構式的函式體之前,已經把該物件的所有成員變數全都初始化好了,盡管如此,但這并不意味著這些成員變數的值與開發者最終想要的結果相符,因為程式僅僅執行了成員變數的初始化陳述句,而尚未執行建構式中與這些變數有關的邏輯,

在構建物件的程序中呼叫虛函式有可能令程式中的資料混亂,也會讓基類的代碼嚴重依賴于派生類的實作細節,而這些細節是無法控制的,這種做法很容易出問題,所以應該避免這樣做,

實作標準的dispose模式

dispose模式用于對非托管資源進行釋放,托管資源是指受GC管理的記憶體資源,而非托管資源與之相對,則不受GC的管理,當使用完非托管資源后,必須顯式釋放它們, 最常用的非托管資源型別是包裝作業系統資源的物件,如檔案、視窗、網路連接或資料庫連接, 雖然垃圾回收器可以跟蹤封裝非托管資源的物件的生存期,但無法了解如何發布并清理這些非托管資源,
比如System.IO.File中的FileStream,它屬于.NET的類被GC管理,但它的內部又依賴了作業系統提供的API,因此可以看作是一個Wrapper, 因此要實作dispose模式,在自身被GC銷毀的時候,釋放檔案句柄,

標準的dispose(釋放/處置)模式既會實作IDisposable介面,又會提供finalizer,以便在客戶端忘記呼叫IDisposable.Dispose()的情況下也可以釋放資源,

在類的繼承體系中,位于根部的那個基類應該做到以下幾點:

  • 實作IDisposable介面,以便釋放資源,
  • 如果本身含有非托管資源,那就添加finalizer,以防客戶端忘記呼叫Dispose()方法,若是沒有非托管資源,則不用添加finalizer,
  • Dispose方法與finalizer(如果有的話)都把釋放資源的作業委派給虛方法,使得子類能夠重寫該方法,以釋放它們自己的資源,

繼承體系中的子類應該做到以下幾點:

  • 如果子類有自己的資源需要釋放,那就重寫由基類所定義的那個虛方法,如果沒有則不必重寫,
  • 如果子類自身的某個成員欄位表示的是非托管資源,那么就實作finalizer,否則就不必實作,
  • 記得呼叫基類的同名函式,

下面兩個類UnManaged與MyUnManaged作為非托管資源的示例,假設UnManaged類中直接使用了非托管資源:

public class UnManaged : IDisposable
{
  private bool alreadyDisposed;

  public void Dispose()
  {
    Dispose(true);
    GC.SuppressFinalize(this);
  }

  protected virtual void Dispose(bool isDisposing)
  {
    if (alreadyDisposed)
      return;
    if (isDisposing)
    {
      // free managed resource here
    }

    // free unmanaged resource here
    alreadyDisposed = true;
  }

  public void ExampleMethod()
  {
    if (alreadyDisposed)
      throw new ObjectDisposedException(nameof(UnManaged), "Call methods on disposed object");

    // do something
  }

  ~UnManaged()
  {
    Dispose(false);
  }
}

public class MyUnManaged : UnManaged
{
  private bool alreadyDisposedInDerived;

  protected override void Dispose(bool isDisposing)
  {
    if (alreadyDisposedInDerived)
      return;
    if (isDisposing)
    {
      // free managed resource here
    }

    // free unmanaged resource here

    base.Dispose(isDisposing); // call base.Disposes

    alreadyDisposedInDerived = true;
  }
}

UnManaged直接使用了非托管資源,因此需要解構式,雖然前面提到存在解構式的物件不會被GC立即回收,但作為一種防范機制是必須的,如果使用者忘呼叫Dispose,finalizer仍然確保非托管資源可以得到釋放,盡管程式性能或許會因此而有所下降,但只要客戶代碼能夠平常呼叫Dispose方法,就不會有這個問題,Dispose方法中通過GC.SuppressFinalize(this)來通知GC不必再執行finalizer,

實作IDisposable.Dispose()方法時,要注意以下四點:

  1. 把非托管資源全都釋放掉,
  2. 把托管資源全都釋放掉(這也包括不再訂閱早前關注的那些事件),
  3. 設定相關的狀態標志,用以表示該物件已經清理過了,如果物件已經清理過了之后還有人要訪問其中的公有成員,那么你可以通過此標志得知這一狀況,從而令這些操作拋出ObjectDisposedException,
  4. 阻止垃圾回收器重復清理該物件,這可以通過GC.SuppressFinalize(this)來完成,

但finalizer中執行的操作與Dispose有所區別,它只應釋放非托管資源,因此為了代碼復用,添加了Dispose的多載方法protected virtual void Dispose(bool isDisposing),它宣告為protected virtual,可以被子類重寫,被IDisposable.Dispose()方法呼叫時,isDisposing引數是true,那么應該同時清理托管資源與非托管資源,finalizer中呼叫時isDisposing為false,則只應清理非托管資源,

還有另外一些注意事項:

  • 基類與子類物件采用獨立的disposed標志來表示其資源是否得到釋放,這么寫是為了防止出錯,假如共用同一個標志,那么子類就有可能在釋放自己的資源時率先把該標志設定成true,而等到基類運行Dispose(bool)方法時,則會誤以為其資源已經釋放過了,
  • Dispose(bool)與finalizer都必須撰寫得很可靠,也就是要具備冪等(idempotent)的性質,這意味著多次呼叫Dispose(bool)的效果與只呼叫一次的效果應該是完全相同的,
  • 在撰寫Dispose或finalizer等資源清理的方法時,只應該釋放資源,而不應該做其他的處理,否則極有可能導致記憶體泄漏等問題,

參考書籍

《Effective C#:改善C#代碼的50個有效方法(原書第3版)》 比爾·瓦格納

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/254635.html

標籤:C#

上一篇:linq to xml怎么查找?

下一篇:《Effective C#》筆記(2) - .NET的資源管理

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • WebAPI簡介

    Web體系結構: 有三個核心:資源(resource),URL(統一資源識別符號)和表示 他們的關系是這樣的:一個資源由一個URL進行標識,HTTP客戶端使用URL定位資源,表示是從資源回傳資料,媒體型別是資源回傳的資料格式。 接下來我們說下HTTP. HTTP協議的系統是一種無狀態的方式,使用請求/ ......

    uj5u.com 2020-09-09 22:07:47 more
  • asp.net core 3.1 入口:Program.cs中的Main函式

    本文分析Program.cs 中Main()函式中代碼的運行順序分析asp.net core程式的啟動,重點不是剖析原始碼,而是理清程式開始時執行的順序。到呼叫了哪些實體,哪些法方。asp.net core 3.1 的程式入口在專案Program.cs檔案里,如下。ususing System; us ......

    uj5u.com 2020-09-09 22:07:49 more
  • asp.net網站作為websocket服務端的應用該如何寫

    最近被websocket的一個問題困擾了很久,有一個需求是在web網站中搭建websocket服務。客戶端通過網頁與服務器建立連接,然后服務器根據ip給客戶端網頁發送資訊。 其實,這個需求并不難,只是剛開始對websocket的內容不太了解。上網搜索了一下,有通過asp.net core 實作的、有 ......

    uj5u.com 2020-09-09 22:08:02 more
  • ASP.NET 開源匯入匯出庫Magicodes.IE Docker中使用

    Magicodes.IE在Docker中使用 更新歷史 2019.02.13 【Nuget】版本更新到2.0.2 【匯入】修復單列匯入的Bug,單元測驗“OneColumnImporter_Test”。問題見(https://github.com/dotnetcore/Magicodes.IE/is ......

    uj5u.com 2020-09-09 22:08:05 more
  • 在webform中使用ajax

    如果你用過Asp.net webform, 說明你也算是.NET 開發的老兵了。WEBform應該是2011 2013左右,當時還用visual studio 2005、 visual studio 2008。后來基本都用的是MVC。 如果是新開發的專案,估計沒人會用webform技術。但是有些舊版 ......

    uj5u.com 2020-09-09 22:08:50 more
  • iis添加asp.net網站,訪問提示:由于擴展配置問題而無法提供您請求的

    今天在iis服務器配置asp.net網站,遇到一個問題,記錄一下: 問題:由于擴展配置問題而無法提供您請求的頁面。如果該頁面是腳本,請添加處理程式。如果應下載檔案,請添加 MIME 映射。 WindowServer2012服務器,添加角色安裝完.netframework和iis之后,運行aspx頁面 ......

    uj5u.com 2020-09-09 22:10:00 more
  • WebAPI-處理架構

    帶著問題去思考,大家好! 問題1:HTTP請求和回傳相應的HTTP回應資訊之間發生了什么? 1:首先是最底層,托管層,位于WebAPI和底層HTTP堆疊之間 2:其次是 訊息處理程式管道層,這里比如日志和快取。OWIN的參考是將訊息處理程式管道的一些功能下移到堆疊下端的OWIN中間件了。 3:控制器處理 ......

    uj5u.com 2020-09-09 22:11:13 more
  • 微信門戶開發框架-使用指導說明書

    微信門戶應用管理系統,采用基于 MVC + Bootstrap + Ajax + Enterprise Library的技術路線,界面層采用Boostrap + Metronic組合的前端框架,資料訪問層支持Oracle、SQLServer、MySQL、PostgreSQL等資料庫。框架以MVC5,... ......

    uj5u.com 2020-09-09 22:15:18 more
  • WebAPI-HTTP編程模型

    帶著問題去思考,大家好!它是什么?它包含什么?它能干什么? 訊息 HTTP編程模型的核心就是訊息抽象,表示為:HttPRequestMessage,HttpResponseMessage.用于客戶端和服務端之間交換請求和回應訊息。 HttpMethod類包含了一組靜態屬性: private stat ......

    uj5u.com 2020-09-09 22:15:23 more
  • 部署WebApi隨筆

    一、跨域 NuGet參考Microsoft.AspNet.WebApi.Cors WebApiConfig.cs中配置: // Web API 配置和服務 config.EnableCors(new EnableCorsAttribute("*", "*", "*")); 二、清除默認回傳XML格式 ......

    uj5u.com 2020-09-09 22:15:48 more
最新发布
  • C#多執行緒學習(二) 如何操縱一個執行緒

    <a href="https://www.cnblogs.com/x-zhi/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2943582/20220801082530.png" alt="" /></...

    uj5u.com 2023-04-19 09:17:20 more
  • C#多執行緒學習(二) 如何操縱一個執行緒

    C#多執行緒學習(二) 如何操縱一個執行緒 執行緒學習第一篇:C#多執行緒學習(一) 多執行緒的相關概念 下面我們就動手來創建一個執行緒,使用Thread類創建執行緒時,只需提供執行緒入口即可。(執行緒入口使程式知道該讓這個執行緒干什么事) 在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的 ......

    uj5u.com 2023-04-19 09:16:49 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    <a href="https://www.cnblogs.com/huangxincheng/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/214741/20200614104537.png" alt="" /&g...

    uj5u.com 2023-04-18 08:39:04 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    一:背景 1. 講故事 前段時間協助訓練營里的一位朋友分析了一個程式卡死的問題,回過頭來看這個案例比較經典,這篇稍微整理一下供后來者少踩坑吧。 二:WinDbg 分析 1. 為什么會卡死 因為是表單程式,理所當然就是看主執行緒此時正在做什么? 可以用 ~0s ; k 看一下便知。 0:000> k # ......

    uj5u.com 2023-04-18 08:33:10 more
  • SignalR, No Connection with that ID,IIS

    <a href="https://www.cnblogs.com/smartstar/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/u36196.jpg" alt="" /></a>...

    uj5u.com 2023-03-30 17:21:52 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:15:33 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:13:31 more
  • C#遍歷指定檔案夾中所有檔案的3種方法

    <a href="https://www.cnblogs.com/xbhp/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/957602/20230310105611.png" alt="" /></a&...

    uj5u.com 2023-03-27 14:46:55 more
  • C#/VB.NET:如何將PDF轉為PDF/A

    <a href="https://www.cnblogs.com/Carina-baby/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2859233/20220427162558.png" alt="" />...

    uj5u.com 2023-03-27 14:46:35 more
  • 武裝你的WEBAPI-OData聚合查詢

    <a href="https://www.cnblogs.com/podolski/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/616093/20140323000327.png" alt="" /><...

    uj5u.com 2023-03-27 14:46:16 more