最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會(huì)員登陸 & 注冊(cè)

第 54 講:類型良構(gòu)規(guī)范(四):`IDisposable` 接口和 `using` 聲明

2021-08-07 21:04 作者:SunnieShine  | 我要投稿

前面我們完成了對(duì) IEquatableIComparable、IEqualityComparerIComparer 接口的基本使用,下面我們介紹一下 IDisposable 接口的用法,以及為什么要有這樣的機(jī)制。

本節(jié)內(nèi)容比較難。按照 C# 的尿性,凡是涉及 CLR(類似 Java 語言的 JVM,即 Java 虛擬機(jī)的一種存在;CLR 是 C# 的運(yùn)行時(shí))的概念和原理上的東西,對(duì)于初學(xué)來說都有些困難。而且本教程是跟編程語言的語法相關(guān)的內(nèi)容,所以 CLR 層面的東西介紹得非常少,也就變成了令人吃力的另外一個(gè)原因。不過,困難總是要來的,你只能去克服。

Part 1 回顧一下 GC 原理

其實(shí)在講指針的時(shí)候我們就已經(jīng)說了堆內(nèi)存和棧內(nèi)存的基本使用規(guī)則,以及堆內(nèi)存是靠 GC(垃圾回收器)的控制自行分配和釋放的,因此無需我們用戶關(guān)心。為了描述表達(dá)下面我們要說的內(nèi)容的意思,我們不得不再次了解它們,以便和后面的內(nèi)容形成對(duì)應(yīng)關(guān)系。

1-1 GC 回收內(nèi)存的原理

回顧一下 GC 的回收內(nèi)存的基本原理。GC 在每隔一段時(shí)間后,就會(huì)自動(dòng)啟用垃圾回收機(jī)制。它會(huì)遍歷整個(gè)內(nèi)存堆上沒有使用到的根(CLR 里把每個(gè)引用類型的單位稱為一個(gè)根,說白了就是一個(gè)對(duì)象就是一個(gè)根)。然后找到這些根后,通過樹狀圖的關(guān)系,把整個(gè)引用這個(gè)根的下面所有元素全部一并提取出來,這些對(duì)象全都是沒有和主程序綁定起來的。既然沒有綁定起來,我們自然可以認(rèn)為它們是垃圾內(nèi)存,于是從根開始,把這顆子樹的所有對(duì)象內(nèi)存全部都釋放掉。

這么說不太好理解,下面我們來拿一個(gè)例子給大家解釋。

如代碼所示,我們簡(jiǎn)單寫了一個(gè)(不完全的)Person 類。因?yàn)楹竺媸桥涮鬃侄蔚膶傩园?、然后是各種方法實(shí)現(xiàn)以及重寫啊之類的,這里我們因?yàn)橛貌簧纤鼈兙蜎]有寫出來。

看到代碼里,我們發(fā)現(xiàn)整個(gè)類型使用到了 _name、_age、_gender 這三個(gè)字段。我們?cè)谧铋_始說過,字段就是存儲(chǔ)和交互數(shù)據(jù)信息的對(duì)象,因此它們也被稱為數(shù)據(jù)成員。這些數(shù)據(jù)成員一旦被對(duì)象實(shí)例化(即調(diào)用 new Person(...) 語句)后,對(duì)象的內(nèi)存就會(huì)得到分配,這些綁定上 Person 類型的數(shù)據(jù)信息也會(huì)放進(jìn)內(nèi)存中。

因?yàn)檫@些數(shù)據(jù)是長(zhǎng)期使用的,所以 C# 不會(huì)像是 C 語言那樣,出了函數(shù)就自動(dòng)釋放內(nèi)存。C# 的引用類型是直接就存進(jìn)堆內(nèi)存的,而值類型的話,如果它是臨時(shí)變量的話,因?yàn)樗粫?huì)長(zhǎng)期使用,因此它們自動(dòng)會(huì)被分配在棧內(nèi)存里,即函數(shù)執(zhí)行期間自動(dòng)分配棧內(nèi)存空間,而方法執(zhí)行完畢后(比如遇到 return 語句了,或者是走到了方法的最后面的閉大括號(hào) } 的地方),方法會(huì)自動(dòng)釋放掉對(duì)應(yīng)的棧內(nèi)存空間,所以為了運(yùn)行速度快,這部分的值類型對(duì)象就會(huì)放進(jìn)棧內(nèi)存;而如果像是上面這樣,_age 這些值類型的字段,它們因?yàn)橐浜蠈?duì)象分配一起使用,所以只能放進(jìn)堆內(nèi)存。因此,我們大概可以這么去總結(jié)一下:

其中,作為值類型的數(shù)據(jù)成員出現(xiàn),堆內(nèi)存或者棧內(nèi)存兩種分配情況都有可能,因?yàn)榉治銎饋砗軓?fù)雜所以這里我們暫時(shí)就不展開講了。

所以,即使上面的 _age_gender 都是值類型的對(duì)象,但因?yàn)樗鼈兪潜蛔鳛橐妙愋偷臄?shù)據(jù)成員出現(xiàn)的,而引用類型的實(shí)例化(new 語句)是必然發(fā)生在堆內(nèi)存的,所以 _age_gender 這兩個(gè)值類型的成員也只能被迫放進(jìn)堆內(nèi)存里。

把這個(gè)搞懂了之后,我們就可以繼續(xù)進(jìn)行后面的內(nèi)容講解了。

1-2 代的概念,以及分代算法

前面的內(nèi)容我們并沒有對(duì)這個(gè)內(nèi)容進(jìn)行講解。GC 為了提高性能,并不會(huì)對(duì)所有不用的對(duì)象全部來一次大搜查。因?yàn)榇笏巡榈男阅芎馁M(fèi)會(huì)非常多,你總不可能用一下程序,用到半截 GC 啟動(dòng)了,開始回收內(nèi)存,然后造成你的程序卡頓吧。這種可見性的卡頓是非常致命的。你想想,連用戶使用都卡頓了,那底層不得翻天,對(duì)吧。

所以,為了盡量滿足需求,GC 還基于這樣的一些假設(shè)來完成性能提升:

  • 占據(jù)內(nèi)存越大的對(duì)象,越常用;

  • 占據(jù)內(nèi)存越小的對(duì)象,越普遍;

  • 回收整個(gè)堆內(nèi)存的無用內(nèi)存比回收其中一部分要慢很多。

第三點(diǎn)就不多說了,這個(gè)正常理解就 OK;下面我們理解一下第一點(diǎn)和第二點(diǎn)是為什么。

首先,我們經(jīng)常用到的是臨時(shí)變量的定義。臨時(shí)變量的定義顯然是方法執(zhí)行完畢后自動(dòng)釋放的機(jī)制。但是引用類型因?yàn)槭欠旁诙褍?nèi)存里,因此不受方法內(nèi)存分配和釋放的自動(dòng)執(zhí)行??蓡栴}在于,這樣的臨時(shí)變量會(huì)有很多,我總不可能經(jīng)常定義的都是一些分配占據(jù)內(nèi)存很多的對(duì)象吧。所以,經(jīng)常都是一些占據(jù)內(nèi)存很少的對(duì)象。這就解釋了第二點(diǎn)了。

而正是因?yàn)槿绱?,占?jù)內(nèi)存越大的對(duì)象,我們肯定也不會(huì)希望這樣的對(duì)象反復(fù)分配內(nèi)存吧。正常情況下,這樣的對(duì)象都是很少的,而這樣的對(duì)象分配出來就是為了經(jīng)常反復(fù)用的。所以,第一點(diǎn)就如這里所述,越大的對(duì)象越經(jīng)常用到。

GC 把掃描根是否正在使用的基本算法模型稱為“根可達(dá)”算法,而正是因?yàn)橛辛巳缟系倪@三點(diǎn)假設(shè),GC 采用了“根可達(dá)”算法的優(yōu)化版:基于分代算法的 GC(Ephemeral Garbage Collector 或 Generational Garbage Collector)。這里的(Generation)是什么呢?

GC 把堆內(nèi)存分配的對(duì)象分為三類:第 0 代、第 1 代和第 2 代。其中第 0 代的對(duì)象是默認(rèn)內(nèi)存分配發(fā)生的情況,換句話說,所有跟堆內(nèi)存有關(guān)的內(nèi)存分配機(jī)制(不管你是什么字段的實(shí)例化,還是臨時(shí)變量的實(shí)例化,只要它要被丟進(jìn)堆內(nèi)存里的話)都是從第 0 代開始的。當(dāng)內(nèi)存分配機(jī)制超出了堆內(nèi)存的存儲(chǔ)大小,或者是 GC 每隔一定時(shí)間后啟動(dòng)了回收機(jī)制,都會(huì)開始檢測(cè)垃圾內(nèi)存。

在檢測(cè)的期間,GC 會(huì)回收掉已經(jīng)不可達(dá)(也就是程序不再使用)的對(duì)象(也就是前面說的“根”)回收掉,然后執(zhí)行“緊湊”處理把中間的 碎片內(nèi)存給消除掉,以免浪費(fèi)內(nèi)存空間;另外,在這之后,GC 還會(huì)自動(dòng)把沒有被回收的對(duì)象從第 0 代改為第 1 代。思考一下,這些第 0 代里的對(duì)象如果要被自動(dòng)回收的話,說明這些對(duì)象基本都是臨時(shí)變量,因?yàn)樗鼈兪亲畛R姷?、即時(shí)聲明即時(shí)使用的對(duì)象類型。啟動(dòng)垃圾回收機(jī)制的時(shí)候,這些對(duì)象顯然會(huì)被盯上,因?yàn)樗鼈冞@個(gè)時(shí)候早已被用完。相對(duì)于垃圾回收機(jī)制啟動(dòng)的時(shí)間間隔來說,方法的執(zhí)行的時(shí)間就顯得非常短,這也是一個(gè)從經(jīng)驗(yàn)上判斷的、為什么臨時(shí)變量會(huì)被優(yōu)先盯上的原因。

一旦對(duì)象被丟進(jìn)第 1 代的話,GC 就不會(huì)再管它們了;換句話說,GC 只去檢測(cè)第 0 代的對(duì)象是不是不可達(dá)。這樣的話,檢測(cè)范圍就會(huì)少一些,GC 的算法的性能就有了提升;而如果讓 GC 去檢測(cè)第 1 代甚至是第 2 代的對(duì)象的話,很有可能什么都檢測(cè)不到,所以干脆就放棄去檢測(cè)它們了。那么這里可能存在兩個(gè)前文沒有提到,但你可能推理出來的 bug。

正是有了這些機(jī)制的存在,微軟把 GC 每次啟動(dòng)垃圾回收的搜索和內(nèi)存釋放的總時(shí)間降到了不超過 1ms,這已經(jīng)算是相當(dāng)了不起的算法了。

1-2-1 如果老代對(duì)象引用新代對(duì)象作為數(shù)據(jù)成員,怎么辦?

如果說,我第 1 代對(duì)象里用到的對(duì)象里的某個(gè)數(shù)據(jù)成員在程序執(zhí)行期間發(fā)生改動(dòng),然后改變了這個(gè)數(shù)據(jù)成員的數(shù)值,引用到了臨時(shí)變量,這可怎么辦呢?

這個(gè)問題有點(diǎn)不好懂。好比我現(xiàn)在有一個(gè) Student 類型,存儲(chǔ)表示一個(gè)學(xué)生的學(xué)號(hào)啊、名字啊、成績(jī)這些信息。然后我又有一個(gè) Teacher 類型,存儲(chǔ)的是這個(gè)人的學(xué)工號(hào)、名字啊、管理班級(jí)這些信息,并且里面有一個(gè) Student 類型的字段,表示這個(gè)人管理的班級(jí)里成績(jī)最好的人的引用。

如代碼所示,我們這里有一個(gè) AddStudent 方法為這個(gè)老師的對(duì)象追加一個(gè)管理學(xué)生。學(xué)生的成績(jī)?nèi)绻饶壳按鎯?chǔ)的最高分?jǐn)?shù)還高的話,那么就更新 _highscoreStudent 字段以及 _highscore 字段的值。為了體現(xiàn)我想說明的問題,我特意把 student 參數(shù)的直接賦值改成了用 new 實(shí)例化(第 20 行代碼)的寫法。假設(shè)對(duì)象實(shí)例化的內(nèi)容是正確、正常的的話,那么這里就會(huì)遇到我前文提到的這個(gè)問題。

原本有一個(gè) Teacher 類型的對(duì)象因?yàn)楦鞣N原因(比如說躲過了第 0 代檢查)被丟進(jìn)了第 1 代的范圍,而此時(shí)假設(shè)我在方法里定義了一個(gè)臨時(shí)變量 currentHighscoreStudent。假設(shè)我現(xiàn)在使用的 this 引用所指代的對(duì)象目前在第 1 代的內(nèi)存里,而這個(gè)對(duì)象的 _highscore 字段原本是 null 現(xiàn)在通過第 21 行代碼變更成了這個(gè)臨時(shí)變量 currentHighscoreStudent 的引用。

可是,我這個(gè) currentHighscoreStudent 可是臨時(shí)變量。臨時(shí)變量就意味著它被 GC 檢測(cè)根可達(dá)的時(shí)候要去看第 0 代里是不是有引用。問題在于現(xiàn)在它是被一個(gè)第 1 代的對(duì)象引用的,而它并沒有在第 0 代里。GC 由于檢測(cè)不到這里去,因此有可能會(huì)因?yàn)闆]有檢測(cè)到這個(gè)數(shù)據(jù)的引用而誤認(rèn)為此時(shí)這個(gè)臨時(shí)變量 currentHighscoreStudent 是沒有引用的(即誤認(rèn)為根不可達(dá)),于是被內(nèi)存釋放了。這個(gè)問題怎么解決的呢?

因?yàn)闄C(jī)制本身就一定不會(huì)檢測(cè)第 1 代和第 2 代的對(duì)象的,因此 GC 在處理這里的時(shí)候就一定會(huì)有漏洞,于是 GC 轉(zhuǎn)去別的地方解決防止漏洞復(fù)現(xiàn)。實(shí)際上,GC 處理考慮到了這一點(diǎn)。怎么解決呢?我們這里是不是一定會(huì)遇到更新數(shù)據(jù)成員的代碼啊?比如這里的第 21 行代碼,我們是會(huì)把 this 引用的 _highscoreStudent 字段更新數(shù)值的。在更新這個(gè)字段的值的時(shí)候,就會(huì)立刻觸發(fā)一個(gè)特殊的機(jī)制:自動(dòng)給臨時(shí)變量對(duì)象打上“用于賦值給對(duì)象的數(shù)據(jù)成員用”的這么一個(gè)標(biāo)簽。只要這個(gè)“被修改方”(即問題里的 this 引用的這個(gè)對(duì)象)是在第 0 代里,那么這個(gè)標(biāo)簽就沒啥用,因?yàn)橐闷饋淼倪@一坨內(nèi)存都會(huì)被釋放掉;但要是這個(gè)“被賦值方”在 GC 檢測(cè)第 0 代后沒有被發(fā)現(xiàn),就說明它一定在第 1 代或者第 2 代里,那么這個(gè)臨時(shí)變量就一定不會(huì)被釋放,而“逃過”GC 檢查,最終被丟進(jìn)第 1 代里。

1-2-2 如果臨時(shí)變量在垃圾回收期間躲過了分析(此時(shí)根可達(dá));但下次可能它就不可達(dá)了,怎么辦?

就這種垃圾回收機(jī)制來說,就算方法執(zhí)行時(shí)間再短,也有可能存在漏網(wǎng)之魚:我現(xiàn)在臨時(shí)變量還在方法里用,很有可能下一回啟動(dòng)垃圾回收的時(shí)候這個(gè)臨時(shí)變量就沒有被引用了。這個(gè)變量按這個(gè)上述說明的邏輯來看,它因?yàn)榈谝淮味氵^了檢查,因而被丟進(jìn)了第 1 代里,但它是確實(shí)需要回收的對(duì)象啊。

這樣的變量很少有這樣的情況出現(xiàn),甚至可以說基本不存在。如果真的存在的話,這樣的對(duì)象也不會(huì)被立馬回收。

這是 GC 算法的 bug 嗎?不是。那這些對(duì)象在第 1 代里就放任存儲(chǔ)不管了嗎?實(shí)際上也不是。在內(nèi)存分配的體系里,第 0 代和第 1 代是有極限的;換而言之,如果你內(nèi)存分配的對(duì)象(在第 0 代或者在第 1 代里)一旦超出了 GC 原定預(yù)算規(guī)劃的大小了的話,GC 會(huì)自動(dòng)啟動(dòng)垃圾回收,這一次垃圾回收并非是“每隔一段時(shí)間”后啟動(dòng)的狀態(tài),而是“被迫”啟動(dòng)的。因?yàn)榇藭r(shí)已經(jīng)放不下數(shù)據(jù)了,CLR 再不啟動(dòng) GC 的話,程序再放對(duì)象進(jìn)去就放不下了。所以 GC 這次會(huì)被迫啟動(dòng)垃圾回收。

如果第 0 代內(nèi)存被占滿了的話,會(huì)啟動(dòng)垃圾回收機(jī)制,自動(dòng)檢測(cè)所有第 0 代里不可達(dá)的對(duì)象,并將其釋放(一定要注意這里的處理機(jī)制是包含了前文 1-2-1 的問題所描述到的這種特殊情況的);而如果第 1 代內(nèi)存被占滿了的話,這個(gè)時(shí)候你再次啟動(dòng) GC 只檢測(cè)第 0 代就顯得有點(diǎn)不夠了:因?yàn)橹惶幚淼?0 代的對(duì)象的話,肯定是有可能存在還在用的對(duì)象(即根可達(dá)的對(duì)象),這些對(duì)象就會(huì)自動(dòng)被移動(dòng)到第 1 代里;但是此時(shí)第 1 代的空間根本不夠放了,所以光搜尋第 0 代的不可達(dá)根是不夠的了。唯有這種情況(連第 1 代內(nèi)存都被占滿了),GC 既會(huì)搜索第 0 代又會(huì)搜索第 1 代的根可達(dá)情況。此時(shí),前面提到的那種漏網(wǎng)之魚就會(huì)在這次搜尋里被發(fā)現(xiàn),并且被釋放掉。

1-2-3 說了這么多,第 2 代怎么沒說呢?

第 2 代的處理稍微復(fù)雜一些。它和前面的內(nèi)存分配不太一樣。下面我們說一下這三代的情況:

  • 第 0 代:所有分配對(duì)象的過程都發(fā)生在這里;

  • 第 1 代:當(dāng)?shù)?0 代的垃圾回收發(fā)現(xiàn)的根可達(dá)的情況,那么它們可能不會(huì)被回收,于是就會(huì)被丟進(jìn)第 1 代里;

  • 第 2 代:如果分配的對(duì)象過大(大概一個(gè)對(duì)象就會(huì)大概占據(jù) 85000 字節(jié)[1]的樣子),或者是第 1 代對(duì)象在 GC 檢測(cè)的時(shí)候發(fā)現(xiàn)的、仍然可達(dá)的對(duì)象,會(huì)被丟進(jìn)第 2 代里。

可見,第 2 代包含的對(duì)象分兩種情況:第一種是被迫啟動(dòng) GC 并查找到第 1 代的對(duì)象的時(shí)候,第二種則是分配超大內(nèi)存的對(duì)象的時(shí)候。如果對(duì)象占據(jù)內(nèi)存過大,這肯定不可能還按普通算法,被丟進(jìn)第 0 代里,然后 GC 檢測(cè),然后放進(jìn)第 1 代里;然后超過第一代容量然后被迫啟動(dòng) GC 然后搜索第 1 代不可達(dá)情況,然后再放進(jìn)第 2 代里這么去移動(dòng)。大對(duì)象是無論何時(shí)占據(jù)內(nèi)存都超級(jí)大的情況。既然如此,我們就不應(yīng)該還按這個(gè)流程走,否則可能第 0 代里,放進(jìn)去就超過第 0 代的存儲(chǔ)容量了。第一代也是如此。所以,第 2 代專門是放置那些大對(duì)象用的。

接著,由于第一代啟動(dòng)垃圾回收機(jī)制的時(shí)候,仍然會(huì)發(fā)現(xiàn)可達(dá)的情況(實(shí)際上大多數(shù)的對(duì)象在第一代里都是可達(dá)的,只有少部分會(huì)被回收,這些少部分的情況就是前文提到的“漏網(wǎng)之魚”)。這些可達(dá)的情況就會(huì)被直接丟進(jìn)第二代里(這個(gè)行為和第 0 代進(jìn)第 1 代的操作行為差不多)。你想想看,第一代都超出容量然后啟動(dòng) GC 都還在用的對(duì)象,是不是對(duì)象一定是很常用,或者是大內(nèi)存對(duì)象???所以,它們被丟進(jìn)第 2 代也是理所應(yīng)當(dāng)?shù)摹?/span>

[1]:這個(gè) 85000 字節(jié)并不是一個(gè)一定準(zhǔn)確的數(shù)值。這個(gè)數(shù)值可能會(huì)在未來 CLR 的算法升級(jí)等等情況下優(yōu)化更替這個(gè)數(shù)值的情況。這個(gè)數(shù)字是作為保守情況說明出來的。

但是。如果第 0 代、第 1 代和第 2 代的內(nèi)存預(yù)算全都被用光了的話,那么程序就會(huì)拋出 OutOfMemoryException 異常類型的對(duì)象來表示內(nèi)存無法繼續(xù)分配。而且,這個(gè)異常是報(bào)告嚴(yán)重錯(cuò)誤的異常,強(qiáng)烈不建議捕獲這種異常;如果要捕獲異常的話,請(qǐng)?jiān)?catch 里追加 Environment.FailFast(-1) 來強(qiáng)制程序立刻在處理異常后自動(dòng)退出。這里的 Environment.FailFast 是一個(gè)靜態(tài)方法,包含在 Environment 類里。這個(gè)靜態(tài)方法用來終止程序。和一般的方法不一樣,這個(gè)方法會(huì)自動(dòng)退出程序執(zhí)行,而不是異常拋出,也不是退出方法執(zhí)行,它會(huì)真的退出程序。而這里的 -1 就是 C/C++ 里 main 函數(shù)的返回值一樣的東西,它表示程序是否正常退出。如果數(shù)值結(jié)果不是 0,就表示程序沒有正常退出。

1-2-4 分代算法里的每一代的容量是定值嗎?

實(shí)際上并不是。GC 的行為特別智能,它甚至還會(huì)去檢測(cè)你的程序的內(nèi)存分配的行為,以此來看你這個(gè)程序的整體到底是不是一個(gè)“經(jīng)常內(nèi)存分配”的程序。如果程序經(jīng)常分配的都是一些瑣碎的臨時(shí)變量的話,那么這些對(duì)象就經(jīng)常因?yàn)榉椒▓?zhí)行結(jié)束而被釋放掉、不再使用它們了。因此,這樣的程序,第 0 代的容量就不用被設(shè)置得特別大。因?yàn)槎急会尫诺袅?,而且也避免了可達(dá)對(duì)象較多然后執(zhí)行緊湊處理而消耗性能。反之,如果 GC 啟動(dòng)的時(shí)候并沒有發(fā)現(xiàn)很多能被釋放掉的內(nèi)存(也就是可達(dá)對(duì)象很多的意思),那么這樣的程序的第 0 代的容量就會(huì)被提高一些。

至此,GC 算法既完美避免了我們可以想到的潛在 bug,還加快了執(zhí)行效率。

Part 2 IDisposable 接口可以出場(chǎng)了

可以從上文給出的 GC 處理機(jī)制的原理里看到,雖然我們知道 GC 能夠有能力處理第 1 代的數(shù)據(jù),但都只是在第 1 代用光的原來預(yù)估的內(nèi)存大小的時(shí)候才會(huì)被迫觸發(fā)一次 GC 清理內(nèi)存。那么如果有些時(shí)候,有一些變量占用的內(nèi)存較大(比如說它直接被丟進(jìn)第 2 代里,或者是不夠大,從第 0 代開始分配,但還是很大,且有可能沒有被 GC 回收的內(nèi)存)的話呢,這些對(duì)象就可能需要我們幫助和輔助 GC 去處理它們。這樣的對(duì)象我們稱為可銷毀對(duì)象(Disposable Object)。這些可銷毀對(duì)象要么較大,不容易被 GC 清除內(nèi)存,要么就是由于內(nèi)存占據(jù)過大而被直接丟進(jìn)第二代里,但需要在用完后釋放內(nèi)存的對(duì)象。

為了讓這些對(duì)象能夠被 GC 更快清理掉,微軟提供了手動(dòng)處理的方式,讓我們使用代碼書寫的方式來在代碼里完成和 GC 交互的過程。下面我們來說一下,如何對(duì)一個(gè)可銷毀對(duì)象完成銷毀。

2-1 初步完成代碼邏輯

首先,我們假設(shè)有這么一個(gè)對(duì)象需要銷毀,這個(gè)對(duì)象的類型是這樣設(shè)計(jì)的:

假設(shè)這個(gè) MyResource 類型就是我們所謂的“占用內(nèi)存較大的對(duì)象類型”,里面的 Component 類型假設(shè)表示的是這個(gè)對(duì)象的需要手動(dòng)清理內(nèi)存的對(duì)象類型。這句話有點(diǎn)繞,我想說的是,任何一個(gè)數(shù)據(jù)類型都是用基本的數(shù)據(jù)類型搭建起來的,但有可能這個(gè)數(shù)據(jù)類型占用的內(nèi)存資源較多,我們就稱為占用內(nèi)存較大的對(duì)象類型(簡(jiǎn)稱大對(duì)象,Large Object)。而這種大對(duì)象類型的東西,為了能夠講解下面的內(nèi)容,我們就需要完成手動(dòng)釋放內(nèi)存的過程,而這里的這個(gè) Component 雖然沒有給出實(shí)現(xiàn),但我們這里是假設(shè)它是一個(gè)大對(duì)象類型;而外部的這個(gè) MyResource 是我們自定義的資源類型,里面的 Component 是模擬的這個(gè)資源類型里面包含的各種各樣的、需要釋放的數(shù)據(jù)。搞清楚我表達(dá)的意思了吧?

下面,我們需要完成釋放的相關(guān)代碼。首先,我們需要對(duì)這個(gè)類型實(shí)現(xiàn) IDisposable 接口。這個(gè) IDisposable 接口里面只包含了一個(gè)名為 Dispose 的方法,長(zhǎng)這樣:

是的,無參無返回值類型。C# 想要讓你在調(diào)用 Dispose 方法后,這個(gè)類型的對(duì)象的內(nèi)存就被釋放掉。因此這個(gè)接口約束你必須要實(shí)現(xiàn)它,才能表示這個(gè)對(duì)象是可銷毀對(duì)象。

我們實(shí)現(xiàn)的代碼是這樣的:

是的,就這兩句話。第一句話 _component = null; 其實(shí)是告訴你,這個(gè) _component 對(duì)象原本引用的對(duì)象立刻脫離整個(gè)程序的調(diào)用和使用。第二句話 GC.SuppressFinalize(this); 是一個(gè)靜態(tài)方法,位于 System 命名空間下的 GC 類型。從名字就可以看出來,GC 類型是專門提供和 GC 交互用的方法集。這里的 GC.SuppressFinalize(this); 是說,我現(xiàn)在正在手動(dòng)釋放這個(gè)對(duì)象的內(nèi)存空間,希望 GC 不要不識(shí)抬舉第三者插足在我手動(dòng)銷毀掉 _component 對(duì)象的內(nèi)存的時(shí)候來干擾我的工作。GC 采用了一種特殊的機(jī)制[1],可以在你程序執(zhí)行期間仍然可以啟動(dòng)垃圾回收機(jī)制,因此我們必須要禁止掉它;否則 GC 有可能會(huì)在你執(zhí)行這個(gè)方法期間活躍起來,開始垃圾回收。

[1]:這個(gè)機(jī)制叫多線程(Multithreading),不過這個(gè)機(jī)制我們只有在后面的內(nèi)容里才講得到,因此這里我們暫時(shí)不多說明這一點(diǎn)。

整體的代碼其實(shí)很簡(jiǎn)單,我們只需要置空所有這個(gè)類型里的各個(gè)成員即可。到以后我們會(huì)慢慢接觸到 C# 新加入的大值類型(值類型的大對(duì)象)之類的東西,這樣的東西也可能因?yàn)榉旁诙褍?nèi)存里而需要手動(dòng)釋放。但是本文因?yàn)椴幌胝f太多(考慮新人入門了解這些內(nèi)容),就不講這么難的東西了。目前你就記住一點(diǎn),只要是這個(gè)資源類型里需要釋放的話,那么我們需要把里面的所有引用類型的數(shù)據(jù)成員全部挨個(gè)置空(即 = null;),然后調(diào)用 GC.SuppressFinalize(this) 來告訴 GC“我這里 this 引用的對(duì)象,你就別管了,釋放內(nèi)存的操作是我在做,你別干擾我”。

那么,整個(gè)實(shí)現(xiàn)邏輯就完成了。怎么自己使用它呢?

很簡(jiǎn)單,用完就調(diào)用 Dispose 方法即可。

所以,整體需要改的地方有兩處。一處是實(shí)現(xiàn) IDisposable 接口,要寫到類型的聲明上面去:

另外一處是 Dispose 方法的代碼實(shí)現(xiàn)。

2-2 解決 bug:潛在的反復(fù)釋放內(nèi)存

上文的代碼實(shí)現(xiàn)有一個(gè)無法避免的 bug,就是用戶可能手賤或者不知情的情況下反復(fù)調(diào)用 Dispose 方法。顯然調(diào)用一次就已經(jīng)足夠了,但是反復(fù)用戶去調(diào)用它,雖然問題不大,但是完全沒有必要。因此我們需要避免這一點(diǎn)。為了避免反復(fù)調(diào)用同一個(gè)方法,我們可以采用標(biāo)記字段的方式。

我們?cè)?MyResource 類型里追加一個(gè) _hasBeenDisposed 字段標(biāo)記這個(gè)對(duì)象是否已經(jīng)完成內(nèi)存釋放。

這個(gè)字段在我們使用這個(gè)類里的其它任何成員里都不去用它,而只有在 Dispose 方法里才會(huì)去使用它。我們使用 true 數(shù)值表示這個(gè)對(duì)象已經(jīng)被釋放,而 false 則表示沒有被釋放。如果被釋放內(nèi)存了,就標(biāo)記成 true;下次用戶再調(diào)用 Dispose 的時(shí)候,直接檢查這個(gè)字段的數(shù)值就可以知道有沒有已經(jīng)被釋放了。

那么,下面就去修改 Dispose 里的方法的源代碼。

我們加上外圍的 if 條件,判斷對(duì)象是不是已經(jīng)被釋放。如果沒有,就釋放對(duì)象,并標(biāo)記 _hasBeenDisposed 字段為 true。最后,別忘了 GC.SuppressFinalize(this);。

一旦重復(fù)調(diào)用 Dispose 方法,就必然會(huì)產(chǎn)生 ObjectDisposedException 異常。這個(gè) ObjectDisposedException 異常是專門用來表示“對(duì)象內(nèi)存已經(jīng)釋放,但你還在使用對(duì)象”的錯(cuò)誤信息。這里剛好用來拋異常。它位于 System 命名空間下,因此你還不用去記額外的命名空間。

那么,這樣就解決了這個(gè) bug。

Part 3 跟此接口使用有關(guān)的新語法

3-1 using 關(guān)鍵字的另一個(gè)用法:using 聲明

當(dāng)對(duì)象已經(jīng)實(shí)現(xiàn)了 IDisposable 接口后,我們就可以認(rèn)為對(duì)象具有釋放內(nèi)存的能力了。那么 C# 提供了一個(gè)語法來專門“用完就釋放內(nèi)存”,以免你忘記手動(dòng)寫上 Dispose。

這個(gè)語法使用 using 關(guān)鍵字開頭,用小括號(hào)表示需要釋放的對(duì)象,然后最后以一對(duì)大括號(hào),表示使用這個(gè)對(duì)象的過程。當(dāng)大括號(hào)內(nèi)的代碼全部完成執(zhí)行,這個(gè)語句會(huì)自動(dòng)調(diào)用對(duì)象的 Dispose 方法來釋放對(duì)象。這個(gè)語法叫做 using 聲明。

if 差不多,這個(gè) using 聲明的大括號(hào)里只有一句話的話就可以省略大括號(hào)。那么假設(shè)原始的代碼大概是這樣寫的:

那么現(xiàn)在有了 using 聲明后,代碼可以這么去優(yōu)化書寫格式:

這么寫簡(jiǎn)單吧。

實(shí)際上,前文的 mr.Dispose() 方法是直接在使用后調(diào)用的。如果 mr 變量在使用過程之中拋出異常的話,此時(shí)資源就會(huì)在拋異常的地方停止執(zhí)行,以至于資源無法正確得到釋放(也就是說,無法執(zhí)行到 Dispose 方法的調(diào)用處)。因此,這樣的實(shí)現(xiàn)是有 bug 的。下面一節(jié)內(nèi)容就會(huì)告訴你真正的等價(jià)書寫格式是如何的。這里的這個(gè)寫法僅供參考。

如果有多個(gè)變量需要一起用的話,可以使用疊起來的方式來表達(dá),比如

或者

如果是同一個(gè)類型的不同變量需要釋放,可以寫在一起,類似于變量定義一樣;但是如果不同類型的話,就只能像是前者那樣,把 using 聲明疊起來用了。

所以,能夠使用 using 聲明語法的條件是,這個(gè)對(duì)象必須實(shí)現(xiàn)了 IDisposable 接口。如果對(duì)象沒有實(shí)現(xiàn)這個(gè)接口的話,即使有無參無返回值的 Dispose 方法也是不行的,因?yàn)?C# 編譯器不知道這個(gè) Dispose 方法是不是用來釋放內(nèi)存的,也可能萬一有一個(gè)處理的代碼的邏輯,名字恰好叫 Dispose 呢。

3-2 異常結(jié)構(gòu)的 finally

很高興,我們終于在這里可以給大家介紹異常結(jié)構(gòu)里沒有說到的 finally 塊了。異常結(jié)構(gòu)里,我們可以使用 try-catch 組合來完成處理,其中 catch 塊可以有多個(gè),只要 catch 后跟的異常的類型不一樣就行。

接下來我們要介紹一種新的異常結(jié)構(gòu)部分:finally 塊。之所以叫“異常結(jié)構(gòu)部分”而不是“異常結(jié)構(gòu)”,是因?yàn)樗钱惓=Y(jié)構(gòu)控制里的其中一小塊內(nèi)容,是語法上的一部分內(nèi)容,并不是單獨(dú)的內(nèi)容。它不像是 ifswitch 是獨(dú)立的,finally 塊的存在跟 if-else 語句的 else 差不多,它必須綁定 if 才可以用;同樣地,finally 塊必須和 try 一起用。

finally 塊是無論如何都會(huì)執(zhí)行的代碼段。它和正常的代碼不同,一旦觸碰異常,我們可以使用 catch 來捕獲異常,但 finally 部分不論有沒有觸發(fā) catch 塊,都是會(huì)執(zhí)行的。它寫在整個(gè) try-catch 部分的最后,和 try 塊和 catch 塊并排,且只能寫在末尾(即不能交換順序):

一般來說,finally 塊是沒有出現(xiàn)的;但是一旦出現(xiàn)的話,不論如何,這段代碼都會(huì)被得到執(zhí)行。我們來看一個(gè)例子。

我們嘗試調(diào)用此方法,程序執(zhí)行順序是這樣的:第 3 行、第 7 行、第 9 行里計(jì)算 b += 80 的操作、第 14 行、第 16 行、第 17 行,最后是第 9 行的 return 操作。注意這個(gè)第 9 行的執(zhí)行順序,雖然是 return b += 80; 要退出方法,但是在 try 塊里,這個(gè) return 語句的執(zhí)行順序就有所變化。

如果這個(gè) try 下面有 finally 塊的話,那么就會(huì)執(zhí)行完 finally 后,最后才會(huì)退出程序。這里的 b += 80 要先執(zhí)行;但 return 的退出方法的效果,卻要等到 finally 塊執(zhí)行完畢了才會(huì)執(zhí)行。

所以,如果有一個(gè) Main 方法里執(zhí)行 Console.WriteLine(Test()) 的話,那么這個(gè)程序的輸出結(jié)果是:

過程如圖所示:

那么,為什么要把這個(gè)控制流程設(shè)計(jì)得這么復(fù)雜和奇怪呢?這是因?yàn)橛行r(shí)候,try 下面可以直接跟 finally,而不需要有 catch 的捕獲異常的部分。

沒有異常捕獲,那么這段代碼拿來干嘛呢?釋放內(nèi)存。finally 塊設(shè)計(jì)出來是為了釋放內(nèi)存用的。在有些時(shí)候,我們不得不需要優(yōu)先考慮釋放內(nèi)存這種嚴(yán)重的問題,才去做一些其它的不是特別重要的行為,比如拋異常。

假設(shè)我有一個(gè)畫筆工具,它的功能是用代碼書寫和在屏幕繪制一個(gè)線條(直線、曲線之類的)。但是顯然這樣的功能過于復(fù)雜,所以消耗的資源也非常多,因此這種類型設(shè)計(jì)出來就必須得實(shí)現(xiàn) IDisposable 接口。接著,由于程序的控制的關(guān)系,用戶可能會(huì)接收到一定的異常,從 try 塊里發(fā)出。假設(shè)代碼大概是這樣的:

假設(shè)我們第 5 行本來是想在屏幕的 (0, 100) 像素點(diǎn)到 (100, 100) 像素點(diǎn)上畫一條直線,但由于 Pen 類型的實(shí)現(xiàn)代碼有誤,導(dǎo)致了異常的拋出。此時(shí),因?yàn)闆]有 catch 塊的關(guān)系,程序必然會(huì)在第 5 行產(chǎn)生一個(gè)異常內(nèi)容。此時(shí)代碼差不多等價(jià)于把第 5 行代碼直接替換為 throw new Exception()。

但是,異常拋出了,資源卻沒有立刻釋放。大家都知道資源不釋放的話,會(huì)在以后留下隱患(諸如內(nèi)存溢出之類的問題),因此我們會(huì)優(yōu)先需要把資源清理掉,然后才拋異常,這才是預(yù)期的行為。try-finally 組合剛好被設(shè)計(jì)成這樣:如果在 try 塊里有異常拋出或者有 return 語句,會(huì)優(yōu)先考慮看是否下面有 catch 塊和 finally 塊。如果有 catch 塊的話,就檢測(cè)拋出的異常是否匹配其捕獲異常的類型;如果匹配的話,那么 catch 的代碼會(huì)得到執(zhí)行,然后檢查有沒有 finally 塊。如果有 finally 塊的話,就會(huì)立刻去執(zhí)行 finally 塊里的內(nèi)容(一般 finally 塊里就是放置一些清理內(nèi)存資源、調(diào)用 Dispose 方法的代碼);當(dāng)包含 finally 塊的時(shí)候,由于會(huì)一定處理這部分的代碼的關(guān)系,try 塊里的 return 或者拋異常的語句會(huì)在 finally 執(zhí)行完畢的最后才會(huì)得到執(zhí)行,但是,如果 return 語句是個(gè)表達(dá)式,就會(huì)先算出表達(dá)式結(jié)果,但此時(shí)不會(huì)返回,要先走了 finally 塊后,才會(huì)把結(jié)果進(jìn)行返回。

很好。知道了 try-finally 的基本執(zhí)行行為之后,我們就可以完整表示上述代碼了。上述 pen 變量的使用和銷毀代碼是用 try-catch 表示的,由于它也包含了資源內(nèi)存清理的基本操作,也保證了資源一定會(huì)在拋異常之前處理掉,因此這才是前面那一節(jié)內(nèi)容提到的 using 的完整等價(jià)寫法。所以,using 聲明等價(jià)于一個(gè) try-finally 的執(zhí)行過程,而其中的 finally 塊一般寫的是處理清理資源內(nèi)存的代碼。比如上面的這個(gè) try-finally 等價(jià)于這么寫:

直接把 try 塊里的代碼照搬過來,using 就會(huì)隱式調(diào)用 Dispose 方法,并且是放在 finally 塊里的。

Part 4 釋放非托管內(nèi)存

4-1 析構(gòu)器的概念

C# 把內(nèi)存分為托管內(nèi)存和非托管內(nèi)存兩類。托管內(nèi)存是 GC 管轄的范圍,但非托管內(nèi)存就是 GC 管不到的范圍了。比如棧內(nèi)存就是典型的非托管內(nèi)存范疇,因?yàn)?GC 不管棧內(nèi)存,這一點(diǎn)我們已經(jīng)心知肚明了。

假設(shè)一種情況。我們可能會(huì)把 C# 的代碼和 C/C++ 交互,以此產(chǎn)生額外的內(nèi)存分配。這些內(nèi)存分配是特殊的,因?yàn)樗赡芡耆荒鼙?GC 管理到(要是一個(gè) C/C++ 執(zhí)行的代碼,分配的內(nèi)存被你 GC 回收了,這程序就完了)。這個(gè)時(shí)候,我們可能也需要手動(dòng)回收內(nèi)存,即使這比較困難。

回收非托管內(nèi)存的時(shí)候,我們不得不需要一個(gè)新的成員類型:析構(gòu)器(也叫終結(jié)器,F(xiàn)inalizer)。詞語析構(gòu)(Finalize)簡(jiǎn)單理解起來,就是釋放內(nèi)存的意思,所以我們說“析構(gòu)一個(gè)對(duì)象”,就等于是在說“對(duì)象的內(nèi)存被釋放掉”,這倆是一個(gè)意思。析構(gòu)器專門用于釋放內(nèi)存,但托管內(nèi)存在我們前文 Part 3 里就全部介紹到了,我們完全可以通過 Part 3 里的代碼實(shí)現(xiàn)來完成這一點(diǎn);所以,析構(gòu)器是針對(duì)于非托管內(nèi)存的一種存在

析構(gòu)器的語法是這樣的:

析構(gòu)器只用于處理非托管內(nèi)存,因此下面我們說一下,如何釋放非托管內(nèi)存。

另外,析構(gòu)器不支持繼承,也不能書寫任何的訪問修飾符,因此一定要注意,析構(gòu)器一定是系統(tǒng)自己調(diào)用的存在,它不是你用來手動(dòng)調(diào)用的。

4-2 具體的釋放非托管內(nèi)存的實(shí)現(xiàn)模式

為了和前文銜接,我們考慮在 MyResource 類型里追加一個(gè)非托管內(nèi)存的指針。為了盡量不使用不安全代碼,我們采取一個(gè)叫做 IntPtr 的值類型對(duì)象,表示這個(gè)指針。這個(gè) IntPtr 和 C 以及 C++ 里的 void* 類型是差不多的意思,它也是表達(dá)任何一個(gè)地址數(shù)據(jù),任何對(duì)象都可以接收(只是說 C# 里分值類型和引用類型兩處處理機(jī)制,所以 IntPtr 多用來表達(dá)值類型的對(duì)象的地址,或者是不安全代碼,即直接是指針變量的裸數(shù)據(jù))。

我們追加該字段:

這個(gè)字段怎么用(比如怎么從外部傳入數(shù)值,它應(yīng)該表示的是什么變量的地址數(shù)據(jù)之類的),這個(gè)網(wǎng)上查一下就可以,所以這里就不啰嗦了。

接著,我們要認(rèn)為這個(gè) _unmanagedObject 的內(nèi)存是非托管內(nèi)存,那么我們需要也釋放掉它。因?yàn)槲鰳?gòu)器只在釋放非托管內(nèi)存的時(shí)候用,而 Dispose 方法既然要處理釋放內(nèi)存,那么托管內(nèi)存和非托管內(nèi)存都需要在里面釋放,因此析構(gòu)器和 Dispose 方法的處理是不同的機(jī)制。為了統(tǒng)一化處理,我們需要在 MyResource 類型里重載一個(gè) Dispose 方法,然后傳入一個(gè) bool 類型參數(shù),表示這個(gè)方法到底處理的是非托管內(nèi)存,還是非托管內(nèi)存和托管內(nèi)存都處理。

因?yàn)橛行r(shí)候,MyResource 可能會(huì)丟給別的類型繼承,所以它可能不是 sealed 修飾的類型;也正是因?yàn)槿绱?,這個(gè)方法需要 protected 來保證對(duì)象不被外界調(diào)用;另外,方法還需要標(biāo)記 virtual 關(guān)鍵字,因?yàn)槟銖倪@個(gè)類型派生下去的話,可能這個(gè)類型會(huì)有更多的數(shù)據(jù)成員需要釋放。所以,這個(gè)重載出來的方法標(biāo)記 virtual 也是有必要的:是為了子類型派生出來的時(shí)候可以重寫掉這個(gè)方法來完成額外數(shù)據(jù)成員的釋放過程。

接著,我們把之前 Dispose 里的代碼照搬過來即可。只是,這里需要改動(dòng)一下邏輯,因?yàn)橛幸粋€(gè) bool 類型的參數(shù)了。

這里我們需要多三個(gè)方法和一個(gè)析構(gòu)器聲明。析構(gòu)器里因?yàn)橹恍枰尫欧峭泄軆?nèi)存,所以調(diào)用 Dispose 的重載方法的時(shí)候需要傳入 false;而正常的 Dispose 方法需要非托管內(nèi)存和托管內(nèi)存都要處理,因此傳入的參數(shù)是 true,并在里面調(diào)用 GC.SuppressFinalize 禁用 GC 回收內(nèi)存。

然后,我們重載出 Disposebool 類型的參數(shù)的對(duì)象進(jìn)去,然后在驗(yàn)證了類型尚未釋放的狀態(tài)后,判斷參數(shù)。如果 disposeBoth 的參數(shù)結(jié)果為 true,則表示這個(gè)對(duì)象是需要釋放托管內(nèi)存的,因此開始釋放 _component 字段(置空即可);而非托管內(nèi)存是必須要釋放的;它和托管內(nèi)存不同。托管內(nèi)存即使你不釋放內(nèi)存影響也不大,因?yàn)樵谝院螅〞r(shí)間足夠長(zhǎng)),GC 就會(huì)發(fā)現(xiàn)到它;但非托管內(nèi)存不受 GC 管轄,所以如果你不去手動(dòng)釋放的話,就會(huì)導(dǎo)致內(nèi)存泄漏的嚴(yán)重內(nèi)存使用錯(cuò)誤。所以這就是為什么 if (disposeBoth) 下面沒有寫 else 部分來包裹非托管內(nèi)存的釋放代碼。

最后,請(qǐng)注意 CloseHandle 方法。在第 28 行上我們引用了一個(gè) DLL 文件里的函數(shù)。這個(gè)函數(shù)是外來的,所以要標(biāo)記 extern 關(guān)鍵字,這一點(diǎn)我們也是在指針處理的最后一節(jié)內(nèi)給大家介紹了這一點(diǎn)。引用了一個(gè)叫做 CloseHandle 的函數(shù),這個(gè)函數(shù)用來清除釋放非托管內(nèi)存;而參數(shù)類型只能寫成 IntPtr 或者 void*,別的類型全部都不行。因?yàn)?void* 是指針類型,在 C# 里屬于不安全代碼,因此我們盡量不使用它們;這里用的是 IntPtr 來表達(dá)。IntPtr 在微軟團(tuán)隊(duì)設(shè)計(jì)的時(shí)候就故意設(shè)計(jì)得和 C/C++ 的 void* 兼容,所以這個(gè)類型也可以放進(jìn)去當(dāng)參數(shù)類型寫進(jìn)去。

然后,這個(gè)函數(shù)是在 Kernel32.dll 文件里,所以我們需要按照之前的內(nèi)容,標(biāo)記 DllImport 特性表達(dá)出這個(gè)函數(shù)是外來函數(shù)。釋放非托管內(nèi)存的話,我們?cè)诘?19 行調(diào)用它。直接把 _unmanagedObject 這個(gè)非托管內(nèi)存的字段傳入,表示這個(gè)指向的內(nèi)存塊要釋放掉;另外,因?yàn)閮?nèi)存已經(jīng)釋放掉,所以 _unmanagedObject 本身的指向也得改成默認(rèn)數(shù)值。如果不改的話,內(nèi)存塊已經(jīng)被釋放掉,但它自己存儲(chǔ)的這個(gè)地址數(shù)值還指向這塊內(nèi)存,這就意味著這個(gè)變量變成了垂懸指針。給這個(gè)字段賦值默認(rèn)數(shù)值的方式有兩種:

  • _unmanagedObject = IntPtr.Zero;:給字段賦值的是 IntPtr 自帶的 Zero 靜態(tài)只讀字段;

  • _unmanagedObject = default(IntPtr);:跟 IntPtr.Zero 等價(jià)的值。

所以,為了完成任務(wù),我們還得注意相當(dāng)多的細(xì)節(jié)。

4-3 整個(gè)對(duì)象內(nèi)存釋放的完整實(shí)現(xiàn)模式

至此我們就完成了整個(gè)模式,釋放內(nèi)存的完整代碼書寫格式。完整的代碼如下:

Part 5 題外話:手動(dòng)啟動(dòng)垃圾回收

最后,你可能想要了解,如何手動(dòng)啟動(dòng)垃圾回收機(jī)制。在前文我們也只是手動(dòng)讓對(duì)象完成垃圾回收的基本處理,但垃圾回收的啟動(dòng)并沒有在代碼里體現(xiàn)。

實(shí)際上,我們只需要調(diào)用 GC.Collect 即可完成手動(dòng)啟動(dòng)垃圾回收機(jī)制。

這樣就可以了。如果你想控制 GC 立刻回收第幾代的話,只需要傳入?yún)?shù) 0、1、2 的其中一個(gè)數(shù)值即可:

但請(qǐng)注意,因?yàn)?GC 只有第 0 代、第 1 代和第 2 代三種情況,因此傳入別的數(shù)值是不正確的,因此程序會(huì)拋出異常中斷程序執(zhí)行。

第 54 講:類型良構(gòu)規(guī)范(四):`IDisposable` 接口和 `using` 聲明的評(píng)論 (共 條)

分享到微博請(qǐng)遵守國(guó)家法律
巴林右旗| 玛曲县| 洪湖市| 河源市| 新乡县| 拜城县| 铁岭市| 禹州市| 泽普县| 太白县| 宁海县| 平武县| 麻江县| 丹寨县| 页游| 和静县| 大连市| 泽库县| 崇阳县| 琼海市| 牙克石市| 望江县| 波密县| 莱阳市| 温宿县| 图片| 十堰市| 酒泉市| 浙江省| 雷波县| 大港区| 上高县| 南雄市| 鹤庆县| 裕民县| 方正县| 石门县| 周至县| 阿图什市| 乌海市| 高邮市|