1. 程式人生 > >C#會重蹈覆轍嗎?系列之4:華而不實的C#析構器

C#會重蹈覆轍嗎?系列之4:華而不實的C#析構器

前段時間去鳥國出差,顛倒黑白,碌碌無為,疏於寫博,請大家理解。下面繼續前貼7月《C與C++社群混戰,C#會重蹈覆轍嗎?》的討論。這次要談的是C#的析構器的問題。這是C#中非常華而不實的一個設計,不必要,且常常誤導很多C#er,且是.NET效能問題的常見陷阱地帶。下面逐項討論:

1.C#析構器是一個醜陋的語法糖

C#析構器(即Destructor)本質上是對Finalize方法的一個override。既然是對Finalize方法的override,那就大大方方讓程式設計師去override 根類Object的Finalize方法好了。可是,C#設計師們首先搞了一個析構器,接著又在編譯器裡面把父類的Finalize方法隱藏掉(你去override的時候,告訴你父類沒有Finalize方法)。但是編譯完後,在IL程式碼中又告訴你override了父類中的Finalize方法,而你寫的析構器卻不翼而飛!

我在程式語言歷史上看到很多語法糖,有些語法糖華麗,有些語法糖冗贅。但是還從沒見過如此彎彎繞的語法糖!

2. C#析構器偏離了析構器原有的意思

析構器自在各程式語言中造始,便有以下兩大基本含義:

(a) 回收物件內部開銷的動態記憶體以及各種資源

(b) 回收具有確定性時刻,比如delete物件時,或者棧cleanup時。

可是C#將Finalize強扭成析構器後,徹底丟失掉前面兩大基本含義,既無法回收動態記憶體,又無法確定時刻呼叫(只能等GC在猴年馬月想起來才呼叫)。而只用於回收資源(而即便連這個任務也完成得很差,參見3.C#析構器不能完成其設計的初衷)。這使得很多沿用以前析構器概念的程式設計師經常犯如下錯誤,比如:

class MyClass { 

        object field;

     ~MyClass()   {  field=null;  }   //既不必要,也嚴重損傷效能

}

class MyClass { 

        object field;

     ~MyClass()   {  GC.Collect();  }   //既不必要,也嚴重、嚴重損傷效能

}

3. C#析構器不能完成其設計的初衷

前面說過C#析構器主要用於釋放物件的資源(非託管資源),而非記憶體。

但很不幸,對於C#析構器這個唯一的任務,它卻不能很好地勝任。因為C#析構器(也就是Finalize方法)是由GC呼叫的,而GC只會在猴年馬月想起來才呼叫(回收物件之前的一輪迴收),往往延誤了物件資源的釋放——而物件資源是非常昂貴的。 如果真的這樣來做的話,專案會倒大黴——比如我們以前的一個專案,有部分程式設計師在析構器中釋放一些native記憶體,最後導致記憶體暴漲——使用者抱怨下來,最後一除錯發現原來都是在析構器惹得禍——這些析構器半天沒有被GC呼叫!

實際上,C#設計者在後來意識到這個問題了,於是又推出來一個Dispose方法(即Dispose模式)來讓使用者顯式釋放資源。然後又推薦程式設計師在Dispose裡面GC.SuppressFinalize(). 即遮蔽析構器。 

既然Dispose能將事情(確定性地釋放非託管資源)做好,析構器如此沒用,當初設計它幹嗎?這是再典型不過的多餘設計了!

4. C#析構器會帶來嚴重的效能障礙

a) C#析構器會將物件的代標記(Generation)拖大,使得物件更難以被GC回收,給GC造成更大效能負擔。

b) 析構器本身釋放資源較晚,造成資源緊張,影響系統性能。

c) 析構器執行需要一個單獨的執行緒開銷,該執行緒的執行(必須時間很短)需要其他執行緒停止,也是一個性能負擔。

這也是為什麼C#推薦實現Dispose,不推薦實現析構器的原因。因為析構器的效能代價太大。可能中小專案的開發人員感受不到這一點,但我相信做過大型專案的朋友,對C#析構器的效能問題會有非常深的體會。

綜上,C#析構器是C#設計師們純粹為了炫耀自己華麗語法糖、而不小心又失了手藝、一個拙劣的設計。

[ Update: ] 聽從網友的建議,把文章中“腦抽型、臭腳、sucks”等“罵街”的話刪除掉了。寫這些“罵街”的話實在是昨晚文章寫到深處,肝火旺盛,想到某些言論,情不自禁而已。並非我就是“潑婦”,今天一看自己昨晚的言論確實火力太猛,接受大家的意見,改正語言風格,希望下面堅持“技術討論不罵街“的原則。如果我有時候情不自禁做不到,希望大家監督指點,我會及時改過自新,重新做人:)