天天看點

物以類聚:對象也有生命相關文章連結物以類聚:對象也有生命

<a href="http://www.cnblogs.com/xiaozhi_5638/p/4268568.html" target="_blank">程式設計之基礎:資料類型(一)</a>

<a href="http://www.cnblogs.com/xiaozhi_5638/p/4268584.html" target="_blank">程式設計之基礎:資料類型(二)</a>

<a href="http://www.cnblogs.com/xiaozhi_5638/p/4268223.html" target="_blank">高屋建瓴:梳理程式設計約定</a>

<a href="http://www.cnblogs.com/xiaozhi_5638/p/4268613.html" target="_blank">動力之源:代碼中的泵</a>

<a href="http://www.cnblogs.com/xiaozhi_5638/p/4554051.html" target="_blank">難免的尴尬:代碼依賴</a>

<a href="http://www.cnblogs.com/xiaozhi_5638/p/4554062.html" target="_blank">重中之重:委托與事件</a>

<a href="http://www.cnblogs.com/xiaozhi_5638/p/4554056.html" target="_blank">可複用代碼:元件的來龍去脈</a>

<a href="http://www.cnblogs.com/xiaozhi_5638/p/4554069.html#b4.2.1">4.2.1 引用與執行個體</a>

<a href="http://www.cnblogs.com/xiaozhi_5638/p/4554069.html#b4.2.2">4.2.2 析構方法</a>

<a href="http://www.cnblogs.com/xiaozhi_5638/p/4554069.html#b4.2.3">4.2.3 正确使用對象</a>

<a href="http://www.cnblogs.com/xiaozhi_5638/p/4554069.html#b4.3">4.3 管理非托管資源</a>

<a href="http://www.cnblogs.com/xiaozhi_5638/p/4554069.html#b4.3.1">4.3.1 釋放非托管資源的最佳時間</a>

<a href="http://www.cnblogs.com/xiaozhi_5638/p/4554069.html#b4.3.2">4.3.2 繼承與組合中的非托管資源</a>

<a href="http://www.cnblogs.com/xiaozhi_5638/p/4554069.html#b4.4">4.4 正确使用IDisposable接口</a>

<a href="http://www.cnblogs.com/xiaozhi_5638/p/4554069.html#b4.4.1">4.4.1 Dispose模式</a>

<a href="http://www.cnblogs.com/xiaozhi_5638/p/4554069.html#b4.4.2">4.4.2 對象的Dispose()方法和Close()方法</a>

<a href="http://www.cnblogs.com/xiaozhi_5638/p/4554069.html#b4.5">4.5 本章回顧</a>

<a href="http://www.cnblogs.com/xiaozhi_5638/p/4554069.html#b4.6">4.6 本章思考</a>

對象的"生"到"死"一直是.NET程式設計中的難點,如果沒有正确地了解對象的生命周期,程式很容易就會出現"記憶體洩露"(Memory Leak)等異常,最終系統崩潰。很多人錯誤地認為調用了對象的Dispose方法之後,對象就"死亡"了,這些錯誤的觀點往往會給系統帶來緻命的BUG。本章将從對象生命周期、對象使用資源管理等方面來說明程式設計過程中我們應該注意哪些陷阱,進而最大程度減少代碼異常發生的幾率。

堆(Heap)和棧(Stack)是.NET中存放資料的兩個主要地點,它們并不是首先在.NET中出現,C++中就有堆和棧的概念,隻是C++中的堆需要人工維護,而.NET中的堆則由CLR管理。在.NET中,我們也稱堆為"托管堆"(Managed Heap),這裡的"托管"與本書前面說到的托管代碼中的"托管"意思一樣,都指需要别人來輔助。

棧主要用來記錄程式的運作過程,它有嚴格的存儲和通路順序,而堆主要存儲程式運作期間産生的一些資料,幾乎沒有順序的概念。堆跟棧的差別見下圖4-1:

物以類聚:對象也有生命相關文章連結物以類聚:對象也有生命

圖4-1 堆與棧存放資料的差別

如上圖4-1,堆中的資料可以随機存取,而棧中的資料必須要按照某一順序存取。

棧主要存放程式運作期間的一些臨時變量,包括值類型對象以及引用類型對象的引用部分,而堆中主要存放引用類型對象的執行個體部分。注意這裡的引用類型對象應該包含兩個部分:引用和執行個體,程式通過前者去通路後者。

物以類聚:對象也有生命相關文章連結物以類聚:對象也有生命

圖4-2 引用類型對象

我們說的一個引用類型對象其實包含兩個部分,圖4-2中的"引用"類似C++中的指針,存放在棧中,而圖中的"執行個體"則存放在堆中。C#語言中的值類型有類似bool、byte、int等等,而引用類型包括一些class、interface以及delegate等等。

<a></a>

上面代碼Code 4-1中int和bool為值類型,是以local1和local3被配置設定在棧中,而A為引用類型(class),是以A類對象引用local2配置設定在棧中,A類對象執行個體配置設定在堆中,如下圖4-3:

物以類聚:對象也有生命相關文章連結物以類聚:對象也有生命

圖4-3 棧和堆中的資料存儲

如上圖4-3,程式中如果需要通路A類對象時,必須通過它的引用local2去通路。

值類型對象與引用類型對象的引用如果屬于另一個引用類型的成員,那麼它們也是可以配置設定在堆中,

上面代碼Code 4-2中,local1和local3依舊配置設定在棧中,由于a和aa屬于引用類型B的成員,是以它們會随B對象執行個體一起,配置設定在堆中,下圖4-4為此時棧和堆中的資料配置設定情況:

物以類聚:對象也有生命相關文章連結物以類聚:對象也有生命

圖4-4 棧和堆中的資料配置設定情況

如圖4-4中所示,雖然aa是值類型,a是對象引用,但是它們都是配置設定在堆中。另外A類對象執行個體也是配置設定在堆中。

棧中的資料會随着程式的不停執行自動存入和移除,堆中的資料則由CLR管理,它們兩者不是同步的,也就是說,可能棧中的一個對象引用已經從棧中移除,但是它指向的對象執行個體卻還在堆中。

注:

[1]關于值類型與引用類型,請參見本書第三章介紹的"資料類型"。

[2]引用類型對象包括"對象引用"和"對象執行個體"兩部分,這種叫法可能跟其它地方不一樣,本書中統一稱棧中的為"對象引用",稱堆中的為"對象執行個體"。另外,堆跟棧的本質都是一段記憶體塊,本節以上幾幅配圖中,堆的"雜亂無章"隻是為了說明堆中資料存儲和通路可以是随機的,突出與棧的差別。

棧中的對象由系統負責自動存入和移除,正常情況下,跟我們程式開發的關聯并不大。本節主要讨論堆中對象的生命期。

上一節中結尾說到過,對于引用類型對象而言,棧中的引用和堆中的執行個體并不是同步的。棧中引用已經被移除,堆中的執行個體可能還存在,我們該怎麼判斷一個對象的生命周期呢?是依據棧裡面的引用還是堆中的執行個體?

答案當然是堆裡面的資料,因為CLR管理堆中對象記憶體時,是根據該對象的目前引用數目(包括棧和堆中),如果引用數目為零,那麼CLR就會在某一個時間将該對象在堆中的記憶體回收。換句話說,堆中的對象執行個體存在的時間永遠要比棧中的對象引用存在的時間長。下圖4-5描述了對象執行個體與對象引用存在時間:

物以類聚:對象也有生命相關文章連結物以類聚:對象也有生命

圖4-5 對象引用與對象執行個體的生存時間對比

圖4-5中對象執行個體生存時間要比對象引用生存時間長,由于在代碼中我們隻能通過引用去通路執行個體,是以當引用從棧中移除後(圖中"可達"變"不可達"處),對象雖然沒有死亡,但是也基本沒什麼用處。

如果再也沒有任何一個引用(包括棧和堆中)指向堆中的某個執行個體,那麼CLR(具體應該是CLR中的GC,下同)就會在某一個時間點對該執行個體進行記憶體回收,CLR的此舉動便是第二章中提到的"管理對象托管資源"(堆中記憶體屬于托管資源,由CLR管理)。需要注意的是,CLR回收記憶體的時間點并不确定,并不是執行個體的引用數目一為零,CLR馬上就對該執行個體進行記憶體回收,是以圖4-5中堆中對象"不可達"狀态持續的時間不确定,有可能很短,也有可能很長。CLR将對象執行個體的記憶體回收後,堆中可能會出現不連續塊,這時CLR還有類似硬碟的"碎片整理"功能,它能合并堆中不連續部分。

物以類聚:對象也有生命相關文章連結物以類聚:對象也有生命

圖4-6 堆中對象的生到死

圖4-6中對象引用R3存放在棧中,指向堆中的D對象執行個體;當棧中R3移除後(注意此時R4必須移除),堆中的D就處于"不可達"狀态,D也成為了CLR的回收目标;當D被CLR回收後,D的位置就空出來,堆中出現不連續塊;CLR随後進行堆空間整理,合并不連續記憶體塊,這便是對象一次從生到死的全過程。注意上圖隻是為了說明堆中D的生命周期,是以堆中其它對象執行個體的引用情況沒有完整繪制出來。另外,如果堆中還有其它的引用指向D,就算R3從棧中移除,D還是不能成為CLR的回收目标,如下圖4-7:

物以類聚:對象也有生命相關文章連結物以類聚:對象也有生命

圖4-7 棧中的引用與堆中的引用同時存在

圖4-7中R3和B中的某一個成員同時指向D,就算R3從棧中移除,D還是處于"可達"狀态,不會成為CLR的回收目标。

很明顯,我們程式中如果要操作堆中的某個對象執行個體,隻能在該對象執行個體處于"可達"狀态時,隻有這個時候對象引用還在,其它任何時候都不行。換句話說,我們能夠控制堆中對象執行個體的時間是有限制的。

CLR還有另外一個功能,它在準備回收一個"不可達"對象執行個體在堆中的記憶體之前,會調用這個對象執行個體的析構方法,調用時間點如下圖4-8:

物以類聚:對象也有生命相關文章連結物以類聚:對象也有生命

圖4-8 CLR調用析構方法時間點

圖4-8中CLR調用不可達對象的析構方法是在它回收記憶體之前,換句話說,對象死亡前一刻,CLR還會調用它的析構方法,如此一來,我們真正能夠操作堆中對象執行個體的機會又多了一處,之前說到的當對象執行個體處于"可達"狀态時,我們可以通過對象引用通路它,現在,當對象處于"不可達"狀态,我們還可以在析構方法中編寫代碼操作它。由于CLR回收記憶體的時間點不确定,是以它調用析構方法的時間點也不确定。"不可達"對象的析構方法遲早要被調用,但是調用時間我們不可控。

物以類聚:對象也有生命相關文章連結物以類聚:對象也有生命

圖4-9 CLR調用析構方法時間不确定

圖4-9中顯示CLR調用析構方法的時間點不确定。

雖然CLR調用析構方法的時間不确定,但是還是為我們提供了一個額外操作堆中對象執行個體的機會。下圖4-10中網格部分表示我們可以在代碼中操作堆中對象執行個體的時間段:

物以類聚:對象也有生命相關文章連結物以類聚:對象也有生命

圖4-10 可操作堆中對象的時間段

由于CLR是根據"可達"還是"不可達"來判斷是否應該回收對象執行個體的記憶體,是以,如果我們在析構方法中又将某個引用指向了對象執行個體,那麼等析構方法執行完畢後,這個對象執行個體又從"不可達"狀态變成了"可達"狀态,CLR會改變原來的計劃,不再把該對象執行個體當成回收的目标,這個就是所謂的"重生"。"重生"是指堆中對象執行個體在将要被CLR回收前一刻,狀态由"不可達"變成了"可達"。下圖4-11顯示了一個堆中對象執行個體的重生過程:

物以類聚:對象也有生命相關文章連結物以類聚:對象也有生命

圖4-11 對象重生

理論上,一個對象可以無限次重生,但是為了避免程式設計的複雜性,"對象重生"在程式設計過程中是不提倡的,也就是說,我們應該謹慎編寫析構方法中的代碼,不提倡在析構方法中再次引用堆中對象執行個體。

注:後面我們可以知道,正常情況下,析構方法除了用來釋放自己使用過的非托管資源之外,其餘什麼都不應該負責。

實際程式設計過程中,引用類型對象使用頻率居高,導緻堆中對象執行個體數量巨大,如果不正确地使用對象,很容易造成堆中對象執行個體占用記憶體不能及時被CLR回收,引起記憶體不足。程式設計中我們應該遵循以下兩條規則:

(1)能使用局部變量盡量使用局部變量,也就是将引用類型對象定義成方法執行過程中的臨時變量,不要把它定義成一個類型的成員變量(全局變量);

局部變量存儲在棧中,方法執行完畢後,會直接從棧中移除。如果把一個引用類型對象定義成局部變量,方法執行完畢後,對象引用從棧中移除,堆中的對象執行個體很快就會由"可達"狀态變為"不可達"狀态,進而成為CLR的回收目标。

上述代碼Code 4-3中,C的DoSomething方法中預設使用的是成員變量(全局變量)_b,當DoSomething方法傳回之後,_b指向的對象執行個體依舊處于"可達"狀态。執行DoSomething方法前和執行DoSomething方法之後,棧和堆中的情況分别如下圖4-12:

物以類聚:對象也有生命相關文章連結物以類聚:對象也有生命

圖4-12 棧和堆中的資料配置設定

上圖4-12左邊為執行c.DoSomething方法時,也就是代碼中NO.1處,棧跟堆中的資料情況;圖中右邊為c.DoSomething方法執行完畢傳回後,也就是代碼中NO.2處,棧跟堆中的資料情況。可以看出方法執行前後,堆中的B對象執行個體都是處于"可達"狀态,根本原因便是指向B對象執行個體的引用是C的成員變量,隻有C被CLR回收了之後,B才由"可達"狀态變為"不可達"狀态。如果我們将DoSomething方法中的_b改為局部變量(代碼中注釋部分NO.3處),情況則完全不一樣:

物以類聚:對象也有生命相關文章連結物以類聚:對象也有生命

圖4-13 棧跟堆中的資料配置設定

上圖4-13左邊為執行c.DoSomething方法時,也就是代碼中NO.1處,棧跟堆中的資料情況,可以看出,指向B對象執行個體的引用b儲存在棧中;圖中右邊為c.DoSomething方法執行完畢傳回後,也就是代碼中NO.2處,棧跟堆中的資料情況,方法執行完畢傳回後,棧中的b被移除,B對象執行個體處于"不可達"狀态,立刻成為CLR回收的目标。

(2)謹慎使用對象的析構方法。析構方法由CLR調用,不受我們程式控制,而且容易造成對象重生,除了下一節中介紹的管理對象非托管資源外,析構方法幾乎不能用作其它用途。

非托管資源和托管資源一樣,都是對象在使用過程中需要的必備條件。在.NET中,對象使用到的托管資源主要指它占用堆中的記憶體,而非托管資源則指它使用到的CLR之外的資源(詳見第二章關于托管資源和非托管資源的介紹)。非托管資源不受CLR管理,是以管理好對象使用的非托管資源是每個程式開發人員的職責。

當一個對象不再使用時,我們就應該将它使用的非托管資源釋放掉,歸還給系統,不然等到CLR将它在堆中的記憶體回收之後,這些非托管資源隻能等到整個應用程式運作結束之後才能歸還給系統。那麼什麼時候是我們釋放對象非托管資源的最佳時機呢?

前面講到過,我們能夠操作堆中對象執行個體的機會有兩個,一個是該對象執行個體處于"可達"狀态時,即有對象引用指向它;第二個是在析構方法中。是以,我們可以在這兩處釋放對象的非托管資源。

由于析構方法調用時間不确定,是以我們最好不要完全依賴于析構方法,也就是說,隻要我們不再使用某個對象,就應該在程式中馬上釋放掉它的非托管資源。為了避免忘記此操作而導緻的非托管資源洩露,我們可以在析構方法中同樣也寫好釋放非托管資源的代碼(作為釋放非托管資源的備選方案)。

代碼Code 4-4中A類型使用了非托管資源,提供了一個公開ReleaseUnmanagedResource方法,程式中使用完A類型對象後,立刻調用ReleaseUnmanagedResource方法釋放它的非托管資源,同時,為了防止程式中沒有調用ReleaseUnmanagedResource方法而導緻的非托管資源洩露,我們在析構方法中調用了DoRelease方法。注意GC.SuppressFinalize方法(NO.1處),它請求CLR不要再調用本對象的析構方法,原因很簡單,既然非托管資源已經釋放完成,那麼CLR就沒必要再繼續調用析構方法。

注:CLR調用對象的析構方法是一個複雜的過程,需要消耗非常大的性能,這也是盡量避免在析構方法中釋放非托管資源的一個重要原因,最好是徹底地不調用析構方法。

如果調用a.DoSomething(NO.2處)抛出異常,那麼後面的a.ReleaseUnManagedResource就不能執行,是以可以改進代碼:

将對a對象的操作放入try/finally塊中,確定a.ReleaseUnManagedResource一定執行。

面向對象程式設計(OOP)中有兩種擴充類型的方法,一種是繼承,另外一種便是組合。二者都可以以原有的類型為基礎建立一個新的類型,這就産生了一個問題,如果是繼承,派生類中使用了非托管資源,基類中也使用了非托管資源,這兩種怎麼統一管理?如果是組合,類型本身使用了非托管資源,類型中的成員對象也使用了非托管資源,這兩種又怎麼統一管理?如果繼承與組合兩者結合起來,又該怎麼去管理它們的非托管資源呢?

在繼承模式中,我們可以将釋放非托管資源的方法定義為虛方法,派生類中隻需要重寫該虛方法,在方法裡面添加釋放派生類的非托管資源代碼,再調用基類中釋放非托管資源的方法即可。上一小節中類型A的代碼改為:

代碼Code 4-6中Abase和A類型都使用到了非托管資源,A類型重寫了父類Abase的DoRelease虛方法,在其中釋放A類型的非托管資源,然後再調用父類的DoRelease方法去釋放父類的非托管資源(NO.2處)。

注:虛方法DoRelease必須聲明為protected,因為派生類需要調用基類的該方法。

基類和派生類非托管資源關系如下圖4-14:

物以類聚:對象也有生命相關文章連結物以類聚:對象也有生命

圖4-14 基類與派生類非托管資源關系

在組合模式中,一個類型可能有許多成員對象,這些成員對象也可能使用到了非托管資源。如果該類型對象釋放非托管資源,那麼其成員對象也應該釋放它們各自的非托管資源,因為它們是一個整體,

代碼Code 4-7中B類型中包含兩個A類型成員_a1和_a2,_a1和_a2都需要釋放非托管資源,由于它們兩個跟B類型是一個整體,是以在B類型釋放非托管資源的時候,我們也應該編寫釋放_a1和_a2的非托管資源代碼(NO.3和NO.4處)。

物以類聚:對象也有生命相關文章連結物以類聚:對象也有生命

圖4-15 組合模式中的非托管資源

上圖4-15中一個對象可以包含許多成員對象,這些成員對象又可以包含更多的成員對象,圖中每個對象都有可能使用了非托管資源,我們的職責就是在parent釋放非托管資源的時候,将它下級以及下下級(甚至更多)的所有成員對象的非托管資源全部釋放。

注:圖4-15中的parent是相對的,也就是說,parent也有可能成為另外一個對象的成員對象。縱觀程式中各個對象之間的關系,幾乎都是這種結構,我們列舉出來的隻是其中的一小部分,圖中childN也可能成為另外一個parent。

繼承與組合同時存在的情況就很簡單了,将兩種釋放非托管資源的方法合并,代碼如下(不完整):

代碼Code 4-8中,A類型派生自ABase類型,同時A類型中包含_b1和_b2兩個成員對象,在A類型内部,我們重寫DoRelease虛方法,首先釋放A中的非托管資源(NO.1),然後釋放成員對象的非托管資源(NO.2),最後釋放基類的非托管資源(NO.3)。

注意,類型ABase的内部結構跟A類型一樣(隻要它們的最頂層基類中有ReleaseUnManagedResource公開方法和對應的析構方法),類型B的内部結構也跟A類型一樣,也就是說,每個類型的内部結構都與A類型一樣。另外,代碼中NO1. NO2以及NO3的順序是可以改變的,換句話說,非托管資源的釋放順序也是可以改變的。

執行個體代碼對應的非托管資源釋放順序如下圖4-16:

物以類聚:對象也有生命相關文章連結物以類聚:對象也有生命

圖4-16 非托管資源的釋放順序

圖4-16中順序号為非托管資源的釋放順序,對于每一個單獨的對象而言,都是遵循"自己-成員對象-基類"這樣的一個順序。圖4-16中非托管資源的釋放順序并不是固定的。

上一節講到了管理對象非托管資源的方法。如果一個類型需要使用非托管資源,那麼我們可以這樣去做:

(1)在類型中提供類似ReleaseUnManagedResource這樣的公開方法,當對象不再使用時,開發者可在程式中人工顯式釋放非托管資源;

(2)編寫類型的析構方法,在析構方法中編寫釋放非托管資源的代碼,防止開發者沒有人工顯式釋放非托管資源而造成資源洩露異常。

既然這些都是總結出來管理非托管資源的有效方法,那麼我們在程式設計過程中就應該把它當做一個規則去遵守。.NET類庫中有一個IDisposable接口,幾乎每個使用非托管資源的類型都應該實作該接口。

"Dispose模式"便是管理對象非托管資源的一種原則,微軟官方釋出類庫中所有使用了非托管資源的類型都遵循了這一原則。該模式很簡單,定義一個IDisposable接口,該接口包含一個Dispose方法,所有使用了非托管資源的類型均應該實作該接口,類似代碼如下:

 View Code

代碼Code 4-9中Abase類實作了IDisposable接口,并提供了兩個Dispose方法,一個不帶參數的普通方法和一個帶有一個bool類型參數的虛方法。如果程式中人工顯式調用Dispose()方法去釋放非托管資源,那麼同時會釋放所有成員對象的非托管資源(disposing參數為true);如果不是人工顯式調用Dispose()方法釋放非托管資源而是交給析構方法去負責,那麼就不會釋放成員對象的非托管資源(disposing參數為false),這樣一來,所有成員對象都得由自己的析構方法去釋放各自的非托管資源。

ABase類型的所有派生類,如果使用到了非托管資源,隻需要重寫Dispose(bool disposing)虛方法,在其中編寫釋放自己使用的非托管資源代碼。如果有必要(disposing為true),則釋放自己成員對象的非托管資源,最後再調用基類的Dispose(bool disposing)虛方法(NO.2和NO.3)。另外對象中每一個方法執行之前需要判斷自己是否已經Disposed,如果已經Disposed,說明對象已經釋放非托管資源,大多數時候該對象不會再正常工作,是以抛出異常。

注:析構方法隻需要在ABase類中編寫一次,其派生類中不需要再有析構方法。如果派生類中有析構方法,也一定不能再調用Dispose(bool)虛方法,因為析構方法預設是按照"底層-頂層"這樣的順序依次調用,多個析構方法多次調用Dispose(bool),會重複釋放非托管資源,引起不可預料異常。

前面提到過,為了確定程式中人工顯式釋放非托管資源的代碼在任何情況中一定執行,需要把代碼放在try/finally塊中。C#中還有一種更為簡潔的寫法,隻要我們的類型實作了IDisposable接口,那麼就可以這樣編寫代碼:

這段代碼Code 4-10編譯之後相當于:

這樣就能確定a對象使用完畢後,a.Dispose方法總能夠被調用。在使用FileStream、SqlDataConnection等實作了IDisposable接口類型的對象時,我們幾乎都可以這麼使用:

在實際開發過程中,我們經常能遇見應用了"Dispose模式"的場合,比如Winform開發中,建立一個窗體類Form1,Form1.Designer.cs中預設生成的代碼如下:

因為Form1派生自Form,Form又間接派生自Control,Control派生自Component,最後Component實作了IDisposable接口,換句話說,Form1間接實作了IDisposable接口,遵循"Dispose模式",那麼它就應該重寫Dispose(bool disposing)虛方法,并在Dispose(bool disposing)虛方法中釋放成員對象的非托管資源(NO.1處)、釋放本身使用的非托管資源(NO.2處)以及調用基類的Dispose(bool disposing)虛方法(NO.3處)。

注:Form1中使用了非托管資源的成員對象幾乎都派生自Component類型,并存放在components容器中,這些代碼基本都由窗體設計器(Form Designer)自動生成,本書第七章中有講到。

總之,記住如果一個類型使用了非托管資源,或者它包含使用了非托管資源的成員,那麼我們就應該應用"Dispose模式":正确地實作(間接或直接)IDisposable接口,正确的重寫Dispose(bool disposing)虛方法。

很多實作了IDisposable接口的類型同時包含Dispose()和Close()方法,那麼它們究竟有什麼差別呢?都是用來釋放非托管資源的嗎?

事實上,它兩沒有絕對的确定關系,有的時候Close的字面意思更形象。比如一個大門Gate類,使用Close作為方法名稱比使用Dispose更直白,是以有時候把Dispose()方法屏蔽,用Close()方法代替Dispose()方法,作用跟Dispose方法完全一樣。

上面代碼Code 4-14中Close方法就起到與Dispose方法一樣的作用,意思便是釋放非托管資源。另外還有一些情況Close()與Dispose()方法同時存在,但是作用卻并不一樣。

總之,凡是談到Dispose(),與它的字面意思一樣,意思是釋放非托管資源,Dispose()方法調用後對象就不能再使用。而談到Close(),它有時候與Dispose()的功能一樣,比如FileStream類型,而有的時候卻跟Dispose()不一樣,它僅僅表示"關閉"的意思,說不定還會有一個Open()方法與它對應,比如SqlConnection。

不管是Dispose還是Close,我們需要注意一點,如果釋放了對象的非托管資源,那麼這個對象就不能再使用,否則就會抛出異常。我們調試代碼的時候偶爾會碰到類似"無法使用已經釋放的執行個體"的錯誤資訊,意思便是不能再使用已經釋放非托管資源的對象,原因很簡單,非托管資源是對象正常工作時不可缺少的一部分,釋放了它,對象肯定不能正常工作。下圖4-17表示人工顯式釋放對象非托管資源在對象整個生命周期的時間點位置:

物以類聚:對象也有生命相關文章連結物以類聚:對象也有生命

圖4-17 釋放非托管資源時間點

圖4-17中調用對象Dispose()方法是在對象處于"可達"狀态時。Dispose()方法調用之前,對象可以正常使用,調用之後,雖然仍然有引用指向它,但是對象已經不能再使用。在圖4-17中"無效"區域操作對象時,大部分都會抛出異常。

注:對象釋放非托管資源後,也就是調用了Dispose()或者Close()方法之後,不代表該對象死亡,這時候還是可以有引用指向它,繼續通路它,雖然此時所有的通路幾乎都已無效。

本章開頭介紹了程式設計中"堆"和"棧"的概念,它們是兩種不同的資料結構,程式運作期間起着非同一般的作用;之後着重介紹了存放在堆中對象以及它的生命周期,該部分的配圖比較多也比較詳細,讀者可以仔細閱讀配圖,相信能更清楚地了解對象在堆中的"從生到死";之後介紹了.NET對象使用到的"非托管資源"以及怎樣去正确地管理好這些資源,章節最後提到了"Dispose模式",它是管理對象非托管資源的有效方式,同時還解釋了為什麼某些類型同時具備Close()和Dispose()方法。對象生命期是.NET程式設計中的重點,清楚了解對象的"生"到"死"是能夠編寫出穩定程式的前提。

1."當棧中沒有引用指向堆中對象執行個體時,GC會對堆中執行個體進行記憶體回收"這句話是否準确?為什麼?

A:不準确。因為GC不但會檢查棧中的引用,還會檢查堆中是否有引用。是以,隻有當沒有任何引用指向堆中對象執行個體時,GC才會考慮回收對象執行個體所占用的記憶體。

2.如果一個類型正确地實作了IDisposable接口,那麼調用該類型對象的Dispose()方法後,是否意味着該對象已經死亡?為什麼?

A:調用一個對象的Dispose()方法後,并不表明該對象死亡,隻有GC将對象執行個體占用的記憶體回收後,才可以說對象死亡。但是通常情況下,調用對象的Dispose()方法後,由于釋放了該對象的非托管資源,是以該對象幾乎就處于"無用"狀态,"等待死亡"是它正确的選擇。

3.如果一個類型使用了非托管資源,那麼釋放非托管資源的最佳時機是什麼時候?

A:當對象使用完畢後,就應該及時釋放它的非托管資源,比如調用它的Dispose()方法(如果有),對象的非托管資源釋放後,對象基本上就處于"無用"狀态,是以一般不能再繼續使用該對象。為了防止遺忘手動釋放對象的非托管資源,我們應該在對象的析構方法中編寫釋放非托管資源的代碼,這樣一來,假如我們沒有手動釋放對象的非托管資源,GC也會在适當時機調用析構方法,對象的非托管資源總能正确被釋放。

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。

本文轉自周見智部落格部落格園部落格,原文連結:http://www.cnblogs.com/xiaozhi_5638/p/4554069.html,如需轉載請自行聯系原作者