一、背景
最近在精讀 《CLR Via C#》和 《Effective C#》 的時(shí)候,發(fā)現(xiàn)的一個(gè)問題點(diǎn)。一般來說,我們實(shí)現(xiàn) IDisposable
接口,是為了釋放托管資源和非托管資源。不過在 C# 類型定義里面有一個(gè)功能類似的東西,那就是終結(jié)器。
最開始我是學(xué) C++ 的,之后學(xué) C# 的時(shí)候發(fā)現(xiàn)這玩意兒不論是寫法和作用,都跟 C++ 里面的 析構(gòu)函數(shù) 一樣。在 C++
里面的析構(gòu)函數(shù)是在對(duì)象釋放的時(shí)候會(huì)被調(diào)用,之后這個(gè)觀點(diǎn)一直被我?guī)У?C#,認(rèn)為資源釋放的動(dòng)作放在終結(jié)器不就行了么。為什么還要我實(shí)現(xiàn)IDisposable
接口,然后讓使用者手動(dòng)釋放呢?
C++ 版本的析構(gòu)函數(shù):
class Line { public: Line(); ~Line(); private: double length; };
C# 版本的終結(jié)器:
public class Line { private double _length; public Line() { } ~Line() { } }
二、原因
說起這個(gè)原因,首先得從 C# 終結(jié)器的 調(diào)用時(shí)機(jī) 說起。終結(jié)器的調(diào)用是 CLR 在進(jìn)行 GC
時(shí),如果某個(gè)對(duì)象寫有終結(jié)器,即便它應(yīng)該被釋放,也不會(huì)馬上回收該對(duì)象。而 C++ 的析構(gòu)函數(shù)是確定性析構(gòu),取決于你調(diào)用 delete 的時(shí)機(jī)。
GC 會(huì)將其添加到一個(gè)隊(duì)列當(dāng)中,單獨(dú)使用了一個(gè) 高優(yōu)先級(jí) 線程去調(diào)用對(duì)象的終結(jié)器。因?yàn)橐WC線程能夠訪問到終結(jié)器對(duì)象,所以本該釋放的對(duì)象,以及對(duì)象相關(guān)的資源就
會(huì)被提升 1 代 ,會(huì) 增加內(nèi)存占用。
一旦終結(jié)器方法帶有死循環(huán),那么 GC 將永遠(yuǎn)無法釋放該資源,造成 內(nèi)存泄漏。
除開內(nèi)存占用增大的原因,如果你在終結(jié)器方法內(nèi)部引用了其他帶終結(jié)器對(duì)象,GC 無法保證終結(jié)器調(diào)用順序,所以你可能訪問到的對(duì)象是已經(jīng)終結(jié)了的。
還有一種情況會(huì)導(dǎo)致尷尬的內(nèi)存泄漏,本來對(duì)象 A 應(yīng)該被釋放了,結(jié)果你在終結(jié)器內(nèi)部又讓其他的根保持對(duì)象的引用,又會(huì)讓這個(gè)對(duì)象復(fù)活。因?yàn)?GC
只會(huì)執(zhí)行一次帶終結(jié)器對(duì)象的終結(jié)器。執(zhí)行一次過后,就再也不會(huì)執(zhí)行對(duì)象的終結(jié)器了。
public class BadClass { private static readonly List<BadClass> _list = new
List<BadClass>(); private string _msg; public BadClass(string msg) { _msg =
(string)msg.Clone(); } ~BadClass() { // 造成 _msg 的內(nèi)存不會(huì)被釋放。 _list.Add(this); } }
三、最佳實(shí)踐
針對(duì) Effective C# 所提出的最佳實(shí)踐,你應(yīng)該為對(duì)象實(shí)現(xiàn) IDisposable
接口,以釋放托管資源。如果你對(duì)象確實(shí)使用了非托管資源,那么你也應(yīng)該為其編寫終結(jié)器。因?yàn)榉峭泄苜Y源的,你不能保證調(diào)用者能夠顯示調(diào)用Dispose()
方法,所以你得通過終結(jié)器來處理。
一個(gè)典型的 Dispose() 方法應(yīng)該將托管資源、非托管資源全部進(jìn)行釋放,設(shè)置對(duì)應(yīng)的標(biāo)識(shí)表明對(duì)象已經(jīng)被釋放了,阻止垃圾回收器重復(fù)清理該對(duì)象、保證方法的
冪等性。
public class FatherClass : IDisposable { private bool isDisposed = false;
public void Dispose() { Dispose(true); // 通知 GC,這個(gè)對(duì)象已經(jīng)完全被清理。
GC.SuppressFinalize(this); } ~FatherClass() { Dispose(false); } protected
virtual Dispose(bool isDisposing) { if(isDisposed) return; if(isDisposing) { //
釋放托管資源。 } // 釋放非托管資源。 isDisposed = true; } public void TestMethod() {
if(isDisposed) { throw new ObjectDisposedException("對(duì)象已經(jīng)被釋放。"); } } } public
class ChildClass : FatherClass { private bool isDisposed = false; protected
override void Dispose(bool isDisposing) { if(isDisposed) return;
if(isDisposing) { // 釋放托管資源。 } base.Dispose(isDisposing); isDisposed = true; } }
在上面的實(shí)踐中,我們提煉出了一個(gè) void Dispose(bool)
方法,并將其設(shè)置為虛函數(shù)。這樣做的好處有兩點(diǎn),第一點(diǎn)是方便子類重寫釋放邏輯,第二點(diǎn)是可以將終結(jié)器和Dispose() 方法內(nèi)部重復(fù)的代碼提煉出來。
熱門工具 換一換