前段時間去鳥國出差,顛倒黑白,碌碌無為,疏於寫博,請大家理解。下面繼續前貼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”等“罵街”的話刪除掉了。寫這些“罵街”的話實在是昨晚文章寫到深處,肝火旺盛,想到某些言論,情不自禁而已。並非我就是“潑婦”,今天一看自己昨晚的言論確實火力太猛,接受大家的意見,改正語言風格,希望下面堅持“技術討論不罵街“的原則。如果我有時候情不自禁做不到,希望大家監督指點,我會及時改過自新,重新做人:)