標籤:

如何評價王垠的 《討厭的 C# IDisposable 介面》?

討厭的 C# IDisposable 介面


雖然我一直覺得王垠這個人混身槽點(我看這文的第一反應是吐槽他竟然不知道.Net Reference Source),但是這篇文章的觀點是要支持的(想看乾貨不想聽吐槽的可以直接跳到正文)。

IDisposable本來就一個很容易誤用,然後誤用了之後又很容易擴散開來的一個pattern。現在.Net類庫里IDisposable的濫用愈演愈烈,我覺得有原因有幾個

  • 有些非常基本的類,當初設計的時候有一些妥協和Trade Off,有不完美的地方,但很多人不明白設計背後的Trade Off,把那個當成最佳實踐來盲從,比如Stream上的Dispose方法,比如說Using語句。我們後面細說。
  • 大公司的官僚文化。大家常常拿穩定性擴展性說事兒,導致有一種「不管有沒有用,反正我現在先加了就不會被說了」的甩鍋心理(某答主很誠實地說出了這種的想法大家可以去看看)。於是在一些不需要用IDisposable的地方用上了IDisposable,產生了一些錯誤實踐。
  • 微軟對Compiler Warning的變態執著(產品編譯全部使用Warning as Error,並且大部分Warning不允許直接Suppress,定期會看Suppress的數量有沒有增加)導致大家為了消除那個Disposable的warning,到處亂加代碼。
  • 上面說的錯誤實踐,因為重複得太多次,反而被當成了正確實踐,最終導致這個問題不可收拾。Ivony的回答,基本上總結了大部分人對IDisposable的看法,可惜的是,絕大部分其實根本是錯誤實踐,正文里有具體的分析。

這個問題嚴重到出現了雖然Task實現了Dispose,但你不應該去調用,這種詭異的情況:

Do I need to dispose of Tasks?

寫C#很久的人,常常因為看過太多這樣的錯誤實踐,已經感覺不到它們的錯誤了,而像王垠這樣的C#新用戶,看到這種錯誤,寫個文章抱怨一下再正常不過了。在這個問題上噴他,我覺得其實挺可悲的。我到是挺期待看到王垠的下一篇,看看他覺得什麼才是正確實踐。

&<&>&<&>&<&>&<&>&<&>&<&>以下是正文&<&>&<&>&<&>&<&>&<&>&<&>

要討論IDisposable在.Net有沒有被濫用,首先我們要知道它是幹嘛用的,雖然大家看似都知道,但常常用起來就忘記了,然後導致很多錯誤實踐,所以我們來一起回顧一下:

IDisposable Interface (System)

Provides a mechanism for releasing unmanaged resources.

重點顯然有兩個,一個是釋放資源,另一個是非託管

明白這個,首先就可以知道,在HashAlgorithm上面, @Belleve 的反對沒有道理,確實需要把數組清零,但這個功能不應該實現成Dispose方法。這個錯誤其實是一個常見的濫用,常常會有這樣的思路:」這個最終的Cleanup是一定要做的,所以我應該放在Dispose里,這樣如果你不調用就會有warning了。「這樣想的人,顯然已經忘記Dispose設計出來只想處理非託管資源這件事了。會犯這種錯誤的人常常是從C++轉過來的,把Dispose和析構的定位混淆了。C++的析構是這樣的:

Destructor (computer programming)

a destructor (dtor) is a method which is automatically invoked when the object is destroyed

它並不管你想在析構函數里做法。那麼為什麼C#的Dispose要定義得這麼精確,而C++的析構卻很寬泛呢?這是因為C++的析構沒有傳播性可以使用得更寬泛些,而C#的Dispose有極強的負面作用要限制它的使用場景。所以說語言和語言雖然很多概念相似,但常常差之毫厘失之千里。

討論清楚這一點之後,我們還可以得到一個很重要的結論:Dispose不等於Cleanup。Dispose只完成了Cleanup當中的一個步驟:釋放非託管資源。一個對象的Cleanup,常常還需要很多其它的操作,將數組清零就是一個例子。.Net也好Java也好,對Cleanup都沒有一個很好的處理辦法。當初大家以為有了GC什麼問題都解決了,結果發現沒了destructor好多地方搞不定,只能用Dispose湊數,前面有提到說Using是一個不好的設計,是因為人人常常會誤以為有了Using就完成了Cleanup。但Dispose模式用來做Cleanup,天然就有缺陷,所以想限制它的使用場景,但架不住大家的濫用。我們在C里已經受夠了這套,C++好不容易解決了,結果C#又回到原點,這真有點諷刺。

明白了Dispose不等Cleanup,Dispose也不應該用作一般意義上的Cleanup之後,我們可以來看一下基類該不該有Dispose方法的問題。

像Stream這樣的基類,他表達的是數據訪問的介面這樣一個概念。基於SRP(Single Responsibility Principle),它不應該承擔」是否需要釋放資源」這樣的職責(Stream實際上應該是一個介面而不是一個基類,用基類來重用代碼已經被實踐證明是有問題的做法,但考慮到Stream出現得太早這也沒辦法)。當有一種具體的實現既實現數據訪問又需要釋放資源的時候,基於ISP(Interface Segregation Principle)它應該同時實現這兩個介面。這樣就不會出現MemoryStream這種明明沒有unmanaged resource,但也要被迫實現Dispose的尷尬。當一個父類或者介面的具體實現是No Operation,或者是Not Implemented的時候,這常常是一個bad smell,說明有設計缺陷。

所以正確的作法是Stream不實現Dispose,FileStream實現Stream和IDisposable,而MemoryStream只實現Stream。大部分情況下,這種實現方法和現在的實現比沒有太大的差。假設我們有一個void ProcessStream(Stream input)來基於Stream處理數據,現在常見的寫法是:

using(Stream s = CreateFileStream(...))

{ ProcessStream(s); ]

換成新的作法要改成

using (FileStream s = CreateFileStream(...))

{ ProcessStream(s); }

或者

MemoryStream s = CreateMemoryStream(...);

ProcessStream(s);

但有一種情況,用新的做法會比較尷尬:

Stream s = CreateStream();

ProcessStream(s);

當有些Stream可以Dispose,而有些不可以的時候,需要把s變成一個成員變數,並引入一個新的變數來保存可以dispose的stream,CreateStream會根據是否dispose來相應的設置,然後Dispose只調用在需要調用的人上,在CreateStream里要這麼寫:

//if FileStream

FileStream fs = CreateFileStream();

this.stream = fs;

this.disposableObject = fs;

// if MemoryStream

this.stream = CreateMemoryStream();

this.disposableObject = null;

另一邊在Cleanup里

if (this.disposableObject != null) { this.disposableObject.Dispose(); }

顯然這樣寫起來比較麻煩。寫程序是一個工程性的事情,常常為了現實而在設計上有所妥協,就像資料庫的範式,雖然模式上當然符合範式最正確,但常常在實際上會選擇有意識地違犯範式。如果考慮到事實上只有MemoryStream這一個不需要釋放資源的特例,而幾乎所有的其它Stream都有釋放非託管資源的需求,於是為了方便,所以在Stream的設計上做了一個妥協,沒有用最正確的法法,而是選了一個相對比較方便的做法。把這種有意識的trade off,當成是一個設計上的最佳實踐,就變成搞笑的事情了。所以基於OO的設計原則,結論是這樣的:

  1. 應該避免在基類和介面上實現IDisposable。基類也好介面也好應該優先保證自己職責的純潔性。
  2. 如果確定所有的子類都會需要實現IDisposable,可以選擇實現IDisposable,但更好的做法是實現interface IFoo, 以及interface IDisposableFoo : IFoo, IDisposable。從而保證decoupling.
  3. 如果你覺得子類可能實現也可能不實現,那就把選擇權留給子類,而不是在基類強制決定。這種虛假的可擴展性,常常是代碼變得完蛋的開始。

接下來說一下,Ivony的回答里的第三個場景。看到有人拿StreamWriter來當好的設計的例子,這還真有點搞笑,要知道StreamWriter/Reader簡直是一個OO設計失敗案例的集大成之作。StreamWriter的設計意圖是好的,它是這麼一個東西:一個可以向任何Stream里寫入數據的工具類。本質上和上面那個例子里的ProcessStream函數是一個東西。我們都知道應該由一個stream的owner來調用Dispose,那麼StreamWriter完全應該不需要去調用Dispose才對呀,代碼怎麼也應該長成下面這個樣子:

using(stream s = CreateFileStream(...))

{

StreamWriter sw = new StreamWriter(s, ..);

sw.Write(...);

}

怎麼StreamWriter就突然需要去Dispose了呢,或者換個問法,怎麼StreamWriter突然變成Stream的Owner了呢?仔細研究一下就會發現,這是因為StreamWriter支持你直接輸入一個文件名,然後它會創建一個FileStream,這才是問題的根源。有基礎OO設計常識的人都知道這是一個特別錯誤的設計,把一個提供通用功能的類,和一個具體的場景給耦合起來了。正確的做法應該是引入一個FileWriter來負責創建FileStream,並調用StreamWriter來實現寫入的功能。StreamWriter就不再需要實現IDisposable,而FileWriter是它創建的FileStream的owner,而FileStream本身是disposable,所以FileWrtier需要實現IDisposable。同樣的這種設計並不需要Stream基類提供Dispose方法。(題外話:每個學C#的新手,都應該吐槽過為什麼StreamWriter不叫FileWriter,導致初學的人根本找不到哪個類是實現這個功能的)

最後總結一下,其實就是兩點:

  1. 有非託管資源的時候要實現IDisposable。
  2. 是一個disposable對象的owner的時候要實現IDisposable。

非要再加一條的話,就是不確定的時候,就不要實現IDisposable。畢竟IDisposable的負面作用太大。

而這兩條原則和王垠推薦的那文里完全一樣。所以王垠對這個問題的看法顯然是正確的。


有一半道理。

其實IDisposable介面現在已經和非託管結構沒必然聯繫了,其實就是一個釋放資源的意思,例如需要unsubscribe一些事件等等。打個比方IObservable&.Subscribe()事件就返回一個IDisposable介面。你說它可以返回另一個介面嗎?當然可以,甚至就一個Action也行,但因為有IDisposable於是就直接用了。

真要說起來,我認為其實IDisposable說到底唯一的作用就是配合using,否則方法叫任何名字都行。但是話說回來,叫任何名字也都解決不了所謂的「傳染」問題,因為它真心需要釋放資源啊。不管你是把資源用Unload還是Close還是Unsubscribe方法來釋放,終究還是會傳染到另一個「釋放資源」的方法上的,即便方法名不叫Dispose。

所以,我現在寫的代碼,假如不是要配合using使用的話,我會選擇不實現IDisposable而是自己定義一個方法,例如叫做CleanUp,或是Unsubscribe,或是Close,甚至還是Dispose但不實現介面。這麼做的好處就是find usage的時候不會把所有對IDisposable或using的地方都找出來,工具可以更容易理解代碼。

最後,IDisposable和Dispose方法其實包含語義上的約定,也就是說調用後對象就銷毀了,不能繼續使用了。假如你叫做Close方法,可能就可以設計成允許重新Open。但假如是Dispose掉,那麼再Open時就可以拋出ObjectDisposedException,告訴你不許再用了。


極大的暴露了王博士完全沒有工程方面的實踐的短板。

順便友情提示一下王博士,.NET Framework本體是開源的,完全不需要什麼神器就能直接查看源代碼,還能直接搜索,點擊瀏覽啥的:

Reference Source

===============================================================

說一下為什麼會有這麼多東西實現 IDisposable吧。

第一個場景,這個類型是非託管資源的一個代理,必須實現

事實上這也是IDisposable介面設計的初衷,作為非託管資源的代理對象,必須在Disposable和析構函數中釋放非託管資源,否則會造成資源泄漏。

典型例子:FileStream。

第二個場景,這個抽象類型大部分實現類都屬於上述場景,這種情況下,為了簡化代碼,即使這個抽象類型本身不屬於上述場景,但也會實現IDispoable介面。

典型例子:Stream

第三個場景,這個類型是上述場景的對象的一個管理器,這種情況下,一般也是要求實現IDispoable介面,但不要求實現析構函數,因為非託管資源會在代理對象回收時回收。

典型例子:StreamWriter、SqlCommand

第四個場景,因為父級抽象類型實現了IDisposable,所以被動實現該介面,但實際上不進行任何釋放動作。

典型例子:MemoryStream。

第五個場景,事實上根本用不著釋放任何資源,但是為了配合using語法所以設計實現IDisposable介面。

典型例子:MvcForm

大家可以自己對一下看看王博士舉的那幾個例子都是屬於哪個場景的。。。

另外HashAlgorithm其實是第四個場景的,儘管它的Dispose方法也有用處。

===============================================================

看到 @Hush 的答案了。

補充一點點,

首先,IDisposable介面被濫用的情況是不是存在?這個必須是要承認的。

但是工程上很多時候,我們追求的是相對完美,而不是絕對的完美,很多東西其實你是很難去取捨的,等到你可以做出決定的時候,大家已經用習慣了,那麼錯的也就成為對的了。

所以,僅僅從IDisposable設計的本意來討論問題和濫用,是相當不妥的。

如果僅僅從本意出發,那麼只有一種情況是合理的,也就是第一種場景,其餘的所有場景都需要進行變通。

第二和第四個場景為什麼會出現,根本原因是using語法的問題。

考慮一下,如果我們只讓FileStream需要實現IDisposable,而Stream則不實現,那麼就會遇到一個問題:僅當一個變數的DeclarationType實現了IDisposable介面才可以使用using語法。

而我們想一下,我們的代碼應當面向Stream編程還是FileStream編程呢?顯然是前者。

結果就是,using這個語法所使用的場景將大大縮減。

事實上這個問題也非常好解決,我們只要讓using語法對任何對象都有效,在運行時再去檢測對象的RuntimeType是否實現了IDisposable介面再作相應處理就好了。

所以這裡就有一個取捨,到底是實現一個空的Dispose方法,但使得IDisposable介面不具備絕對的語義?還是運行時檢查類型,使得using語法行為動態調度?

我不想說哪個好,我只想說,C#是這樣選擇的。

假設我們設計了一個虛擬的文件系統(內存中運轉),再假設這個虛擬的文件系統並不需要任何非託管資源(暫且不討論這個設計是否合理),那麼我們很應該在FileStream的基礎上繼承,然後這個派生的類型將失去非託管資源的代理身份,又使得IDisposable介面的語義不純粹了。

所以,一切皆是取捨。

當然,FileStream的設計本身就是有問題的,但是離題太遠我們暫且打住。

最後多句嘴,事實上foreach語法對於IEnumerator/IEnumerable介面對象就做了RuntimeType檢查IDisposable介面是否實現並進行動態調度的展開。

第三個場景也是可以避免的,譬如說我們可以設計一個叫做IDisposableObjectOwner的介面,這樣就完美避開了IDisposable的語義問題,但是這在工程實踐上是否有意義?

第五個場景,為什麼我們不另外發明一種語法呢?譬如說scope( xxx ){ ... }。

在工程實踐上,我們很多時候是追求一個平衡,在不把問題搞成一團糟的前提下儘快的把東西做出來。using和IDisposable有很多的設計缺陷,但是和C#這個語言本身的各種缺陷比起來,還真算不上是特別突出的。只不過會讓某些有潔癖的人不爽而已,但是如果真的有語言潔癖,就不應該用C#這種簡單粗暴、毫無藝術感、專門為工程實踐所設計的語言了。

.NET Framework是一個如此龐大的工程項目,你不可能指望:

1、每個人都有清醒的頭腦知道自己在幹什麼,能夠嗅到代碼中的壞味道,能夠及時發現這些語義層面上的問題。

2、每個人都能停下手頭的工作然後和語言設計組一起討論如何改進語法讓我們的東西看起來更像是藝術品。

.NET Framework很大程度上就像是一個前衛的建築物,你遠遠的看著很像藝術品,湊近了看不過是一坨坨鋼筋水泥粗暴的堆砌在一起。上面還有各種毛刺。。。

一定要說這是個問題,這個問題的源頭在於using語法與IDisposable強語義介面的綁定。事實上真要解決,我覺得只需要去除這個綁定就可以了,使得using語法針對IClosable介面生效,然後現有的非託管資源代理類,在實現IDisposable的同時,再實現IClosable介面,並調用Dispose方法問題就解決了。

Task的那個問題,我覺得問題根本出在Task被作為了async方法的返回值上,理論上來說Async方法根本沒有任何必要返回Task。只不過因為Task正好解決了一大票問題,例如異常傳遞,返回值等等,而在Task之上並沒有設計合適的抽象,拿來用算了。


因為文章中途修改過,這個回答主要是針對原文中質疑「ManualResetEvent, Semaphore, ReaderWriterLockSlim」這些對象為什麼要實現IDisposable的部分。這些對象內部都使用了Windows句柄,指向Windows的Event對象,因此佔用了內核資源,從原理上看它跟不再使用但沒有關閉的文件、socket、殭屍進程的性質差不多,如果有太多句柄沒有被回收,也會造成服務甚至系統內核崩潰。IDisposable不應該在沒有非託管資源的類上實現,這個說的很對,但是前面舉的幾個例子,偏偏都是有託管資源的情況,在這些情況下,GC是幫不上忙的。這是整個答案的總提綱。

=================================================================

驚訝,就算是寫Java的程序員,難道會把文件打開著等著gc去關閉嗎……

說IDisposable的設計有缺陷這個可以理解,因為的確一層一層嵌套去做析構挺麻煩的(雖然C++程序員會表示少見多怪)。如果不能理解他舉得幾個例子為什麼需要IDisposable,那就有點奇怪了。

即使只做Linux開發,也會明白及時關閉文件和socket的重要性:文件號數量是有限的,還經常嚴格受到ulimit的限制,而gc通常的實現是在一個低優先順序的線程中進行的,這意味著在系統壓力大的時候,gc是跟不上的,而系統壓力大的時候也同時是文件號吃緊的時候,如果把這個任務扔給gc,分分鐘就是重要服務崩潰甚至系統崩潰的節奏。

Windows下也是一樣,輪子哥說不知道為什麼Windows不解決這個問題,我覺得這壓根就是個無解的問題。句柄指向的是Windows的內核對象,包括File,包括Socket,包括Event、Mutex——是的,如果使用線程同步的對象基本全部都有內核部分的實現,不僅是Windows,Linux也一樣。句柄通過引用計數保存了一個對象在內核中,這個對象可能有多個句柄指向它,甚至可能被多個進程共享(想一下GetHWnd),內核一旦內存溢出,那可就出大事了。所以任何時候對於使用了系統句柄的對象,C#都會希望它能被及時釋放掉。涉及密碼學的則是另一件事,密碼學中使用的所有臨時對象都必須被立即清零,這在C++當中也是一樣的,以至於有時候要專門定義不允許被編譯器優化的SafeZeroMemory之類的介面。

另外一些吧,我覺得完全得讓Java背鍋——Java怎麼躺著也中槍了呢?

因為C#的介面設計是模仿Java的,而Java的多態相當不靈活,比如說介面繼承這個問題。我們規定所有使用using語法的對象都要實現IDisposable,然後我們又希望所有的Stream都可以使用using,那麼我們必須讓Stream介面繼承IDisposable。這有的時候就坑了,比如說MemoryStream也是Stream,結果MemoryStream也必須實現IDisposable了。

總體上來看,我覺得這篇文章當中說的問題,屬於沒有理解到使用句柄對象的危險性——如果某個設計在一個到處被引用的臨時對象里保存了一個打開的文件,估計會被人狠狠敲腦袋;但是許多人卻認識不到在一個到處被引用的對象里保存一個Event對象或者ReadWriteLock之類的對象也是同樣危險的。但是後者卻是多線程需要同步時最簡潔的設計,這也是一種悲哀吧……

針對這種有很多對象需要同步的需求,我覺得也許可以考慮採用一種「鎖池」的設計:

設計一個叫做LockService的類,用於提供全局鎖服務,可以設計成Singleton的,也可以按不同namespace提供多個實例。裡面提前創建很多很多個同步對象,放進一個空閑表裡。同時有一個HashMap保存當前使用的鎖。當請求鎖服務的時候,需要提供一個字元串(或者其他對象類型,需要可以hash)的key,所有key相同的請求會互鎖;LockService類收到這個請求之後,首先判斷這個key是否在HashMap中,如果在,則取出HashMap中保存的鎖進行Lock操作,同時將這個key的引用計數加一;否則從空閑表中取出一個空閑的鎖放進HashMap中。Unlock的時候,將引用計數減一,如果引用計數減到了0,則從HashMap中歸還這個鎖。如果空閑表中鎖的數量不足則擴大鎖的規模,空閑表中空閑數量過多則釋放多餘的資源並縮小規模。HashMap自己可以用一個ReadWriteLock來保護。

這樣設計的好處是只有實際正在實用的鎖才會佔用資源,而大部分可能被鎖但暫時沒有被使用的鎖只是邏輯上存在這個「鎖」而不佔用句柄,總的句柄數可以被控制,也繞開了IDisposable的問題,在這個基礎上再包裝一個返回一個IDisposable對象的介面用來在using中使用,再包裝一個LockObject類封裝一下,就可以得到一個沒有IDisposable介面的鎖對象了。LockObject加鎖的時候使用自己作為key,就可以不用想怎麼設計一個不重複的key的問題了。

嗯,不過看上去蠻蠢的。

=================================================================

今天又看了一遍修改完的最新版本,比起最早的版本來說還是清晰很多了,不過對於後面文件和Event對象的討論還是有一些不能認同的地方。

作者說,File只所以要關閉,是因為跟其他程序的訪問是互斥的,這個有一定道理,但顯然不是全部的理由,如果我這個文件只有我自己用呢,是不是就可以不關閉?如果是個臨時文件呢?如果不是文件,而是socket呢?事實上我們都知道,不管是Linux還是Windows,文件號數量都是有限的,如果一直打開不關閉,很快就會報too many files然後崩潰了。之所以要及時回收,是因為我們知道這是一種有限的資源,如果不回收可能就會崩潰。

那麼為什麼內存可以讓GC來回收呢?這跟GC的機制是有關的,我們知道GC一般需要進行全對象的標記掃描,這是非常昂貴的操作,不能在每次修改引用的時候立即進行,所以GC會延遲到之後進行,其中的一個條件就是:分配內存時,發現內存不足。這是因為託管程序的內存分配完全是由.NET Framework(或者JVM)進行的,所以託管框架對於內存使用量心知肚明,可以在合適的時候進行GC。對文件和句柄就不是這樣了,託管框架並不知道什麼時候打開文件或者句柄已經接近上限,也沒法在這個時候觸發GC——這些對象佔用的資源和內存並不在託管框架範圍內。所以,盡情佔用內存等待GC是安全的,而盡情佔用句柄、文件號等待GC是不安全的。

Event再小,那也是個句柄,作者大概還沒有太多Windows下的開發經驗,不知道「句柄泄露」這件事情多麼可怕,而且觀察程序佔用內存這件事情本身就是不太正確的,泄露的句柄佔用的是內核內存。另外,許多時候交給GC來回收句柄並不會表現出有大風險,這一般都是因為壓力沒上去,不管怎麼說,把身家性命交在一個有隨機性的、不知道會不會觸發的程序手裡,還是說不過去的。

===================================================================

話說,第一次看到的版本裡面說後面會出一個「終極解決方案」的,怎麼吃了……


純惡搞:

開一個新的標記介面叫 IImplementedIDisposableButDoesNotReallyNeedIt。如果 IDisposable 和這個標記介面同時出現,則意味著這個類由於這樣或那樣的原因無奈實現了 IDisposable,但就算不調用 Dispose 方法也不會出現洩漏。

如果要分清類別,還可以有 IImplementedIDisposableToManageOtherIDisposables、IImplementedIDisposableToConvinceYouToUseUsingBlock、IImplementedIDisposableBecauseIDontKnowShitAboutMyCode。


不同意。

1. HashAlgorithm是父類,如果實際使用的演算法是在運行期決定的(很有可能),那麼它有責任為所有實現者規定一個統一的資源管理模式,而有的子演算法可能確實需要釋放資源。它自己沒有資源釋放並不影響這種設計;

2. 照我的理解,把內存清0並不是為了釋放內存,而是為了避免攻擊者從內存讀取重要數據,因為加密演算法的數據一定是攻擊者的重要目標,不用時馬上清0從安全形度是非常有必要的,這個事情不能等GC來做。

補充:王垠的文章也提到安全問題,認為Dispose是用來釋放資源的,不應該和安全攪在一起。其實從Net社區的實踐來看,Dispose現代的語義基本相當於「確定性析構」,並不見得一定要有資源。比如http://ASP.NET MVC裡面就有 using (Html.BeginForm()) 的用法,它實際上和資源沒什麼關係,但用起來確實方便,也沒人認為這樣做有什麼不妥。


王垠是不是不知道Windows的各種句柄數量上限其實是很少的,而且是全局的,不是每個線程單獨算的,不主動Dispose的話可能全部軟體一起完蛋(逃

09年的時候,還經常發現用著用著,記事本打開了之後就沒有菜單的事情,因為HWND不夠用了。還有各種HANDLE其實也是差不多的。不知道為什麼Windows到現在都不想理這個問題,估計是因為反正大家都一定會使用using語句。

話說回來,.net發展到了今天,IDisposable已經跟非託管資源沒什麼關係了,他已經變成了「可以被using」和「一次性事件」的標準用法(逃


HashAlgorithm 我反對,清零是為了避免從內存讀除數據從而預測各種 hash 的值或者偵測輸入,用於加密的場合這個非常重要

另外你覺得 using 要多包一層很難看就直說,然後去給 Roslyn 的 Proposal: "using" local variable declaration (RAII) · Issue #5881 點贊


using有沒有濫用另說,但是他這篇文章,寫的實在很掉價,GC只能感受內存壓力,不會感知系統資源壓力,可能windows全局系統資源耗盡了,對於內存壓力也只是一點點,從而不會引發GC,整個系統全盤完蛋。我感覺,微軟的朋友要看到了,至少讓他工資降低一半。


別的不說,這次文章寫得理性又謙遜了,還炸出這麼多大牛來一起好好地聊技術 (也只是順帶黑了一下不知道 reference source 罷了)。

這本身就是進步了,是個好事兒。

說回這個介面,嗯,的確是被濫用了。變成了 因為我要用 using ,所以我要帶這個介面,順便實現一下析構,底層怎麼析構,我也不知道,我要是把GC理解錯了,再說吧,反正我就是要用 using,順便把析構寫了放在那兒了。

歡迎繼續撰文討論或吐槽C#的設計,讓社區更加活躍。像類似的吐槽,另一個大牛Jon Skeet做了很多,甚至有影響新版本的設計呢,哈哈!

https://codeblog.jonskeet.uk/category/evilcode/


作者討厭的只是濫用而已。


IDisposable本來是設計來顯式釋放非託管資源(比如句柄)的,因為GC存在不確定性,在析構函數/Finalize里釋放非託管資源是不科學的。於是搞了一個IDisposable來顯式的控制非託管資源的釋放。這個出發點我覺得是好的。

於是,這個應該是一個「設計是不是要為濫用背鍋」的問題。

但從另一個角度來將鍋還是不能完全甩掉,因為IDisposable事實上具有傳染性,如果我實現的一個封裝類持有某個IDisposable的實例,那麼很有可能我的這個類也需要實現IDisposable了,於是……這個不能說是完全的好,還是完全的壞,比如Java設計為throw具有傳染性,也不能簡單的說是好還是壞。


原來垠神又改文章了←_←

以下內容作廢

再一次印證了知乎大神們讀王垠的文章就像是新聞記者讀科研報告,只喜歡讀半句。

1、王垠說「不應該在Dispose里做加密敏感數據的cleanup」,到了知乎大神的筆下變成了「王垠認為加密演算法中的臨時內存不應該cleanup」。

2、王垠說「在Dispose里把指針設置為null是沒有用的,不會導致GC立刻回收此資源;而且這麼做等於是在不僅沒有回收資源,還可能意外提早設置了null導致錯誤,等於回到了無GC語言還沒有性能提升」,知乎「大神」卻認為「Dispose可以立刻回收內存,提高性能,王垠缺乏工程經驗,不知道這一點」。

3、王垠說「IDisposable介面由此可見是被濫用了,還有傳染性,Java裡面記得關文件就可以了,到了C#裡面竟然要做這麼多沒必要的Dispose」,到了知乎大神的筆下變成了「王垠認為不應該有IDisposable介面,說明他作為Java程序員連文件是非託管資源需要手動關閉的意識都沒有」。

4、王垠使用了dotPeek,知乎大神立刻找到了噴點,「王垠果然是對C#不熟悉,不知道Reference Source」。

5、王垠說「.NET有缺陷」,知乎大神立刻回答「.NET太龐大,權衡的結果」,吐槽都不可以了!

我真為很多人的閱讀理解能力感到拙計啊!


原則上來說,任何非託管資源,都可以被封裝成託管資源。所以非要對「非託管資源」暴露一個介面,只能是因為這個資源太稀少,不能等待 GC 的啟動時機。

所以這裡就涉及幾個問題:

  1. GC 優化的好不好。GC 應該是盡量頻繁的短時間運行。典型的「分代 generational」GC 就是為了這個設計的。所以沒道理 GC 不能為稀缺資源進行優化。
  2. 系統本身設計有沒有缺陷。像 HWND 用光這個問題,簡直就是羞恥。系統 handle pool 預定過小不說,a button is a window 本來就是非常傻的設計。你要用這個來反駁改變現狀困難還行,反駁王垠嘲笑你那就正撞槍口了。
  3. 即使對稀缺資源,disposable 和 GC-manage 應該是共同作用。Disposable 屬於給程序員一個手動優化的選項,而不是不掉用就出錯的東西。

其實我也奉勸各種用 GC 的平台,要是對付必須及時釋放的資源,你們能不能就 fallback to reference counting ?自動 ref-counting 語言不支持,就算暴漏一個手動 counting 介面也是給程序員一條活路啊。

你們覺得 root-tracing GC 是香餑餑,也不能一遇到稀缺資源就退回到吃手動管理這泡屎吧?


王垠同學好歹也寫了這麼多年程序了,理論方面我不評價,但是工程方面實在是沒什麼進步。


好多人都沒認真看王總的博客就開噴,王總噴的是對IDispose介面的濫用,而不是IDispose介面本身。

作為多年C#使用者,大體上是支持王總的,我也是經常看到有人在C#里把對象用完設置成null覺得很不可理喻。浪費那麼多行代碼做內存管理該做的事。

至於噴資源限制的人,希望你們看十遍這篇博客再噴,謝謝,王總哪裡說不能用於非託管資源釋放了?


我覺得兩邊都有道理,但是說的不是一回事。

偶像抱怨的是強制性的介面方法實現要求你寫一堆呆板「無用」的垃圾代碼(應該被自動化取代)。

知友反駁的是文件、網路、資料庫,不釋放只能等死。

雙方都不算錯,但是這次我還是要一如既往的支持偶像。

你們難道不知道偶像在編程語言靜態分析方面的絕世武功天賦么,我相信他說他已經有一套方法,絕對不是「這裡地方太小,寫不下」,應該是能夠以自動化的方式去處理此類問題。比如用一個分析器+自動代碼修訂,自動編寫需要的Dispose;又或者,外掛一個運行時的補丁,一方面能夠讓Roslyn 閉嘴,同時能在運行時妥善處理資源釋放問題。類似在資料庫連接池裡加一個主動回收資源的進程,回收那些忘記close的鏈接。

這種設計我覺得是合理的折中,畢竟不是所有人都有偶像一樣的實力,隨意發個功就可以給語言、VM打補丁。強制實現這個介面,就是一份「格式合同」,要求你必須承諾你會承擔不及時釋放的責任,至於是不是真的釋放的及時正確有效率,才管不了那麼多。

最後,偶像就是有人氣,不知多少爬蟲在爬http://yinwang.org,破乎的帖子更新時差越來越短了。


GC只管內存。王垠不會連這都忘記了吧。

準確來說,GC和資源管理其實不是一個範疇的東西。GC管理的是邏輯對象,因為他們需要佔用內存,才會和內存管理搭上關係。而資源管理是實實在在地考慮各種真正的資源的分配,使用和回收。內存只是其中一種。

內存不同於別的資源,成本低,總量大,衝突少,浪費一點可以接受,所以可以寫一個自動管理他的GC設備。其他的資源,GC演算法通常無能為力,必須自己管理。不然的話,文件句柄,資料庫連接,互斥鎖這樣價格高昂或者隨時可能衝突的資源,你敢讓一個也許5毫秒,也許5小時才釋放資源的GC去管理?

IDisposable是留給你的資源管理代碼使用的系統設施。把GC拉出來抱怨只能說明不了解情況,對資源管理還有不切實際的幻想。


是的,我也遇到這個問題。明明不管理非託管資源,也要IDisposable一下

【發明者是否應該為濫用背鍋】還有待商榷,但濫用確實是坐實了的。而且我自己也在濫用。

我濫用的初衷是:

1.首先是甩鍋。

就算我現在沒有用非託管資源,誰知道我今後會不會用?到時候我再實現IDisposable,之前使用到我的人怎麼辦?

所以,不管有沒有用,我第一版就實現IDisposable,將來出問題誰沒Dispose誰活該。

2.還是甩鍋

只要我有任何IDisposable成員變數,那我就必須實現IDisposable。誰知道那些成員對象內部做了什麼,反正我先實現了,你隨意


不該有的東西就是不該有,你有一萬個理由,它還是不該有。

這是系統設計跟語言設計哲學上發生了衝突,一個追求高性能,確定性,一個追求自動化。沒辦法,暫時語言還得委屈一下,直到它們不再依賴系統實現的時候。

說王垠工程不足是不理解他的理想主義特質,雖然在GC這個點上,我持否定態度,和他以及很多和反對他的人都對立。

補充:我早就說過,一切皆引用/GC在邏輯上是說不通的,這種偷懶到底的做法不僅無法轉移邏輯編碼中的本質矛盾,也明顯低估了人和機器的智能性。

參考:內存(資源)會泄漏嗎? - Hierarchy - 知乎專欄


推薦閱讀:

如今 Windows 軟體開發究竟該用什麼庫,C#、Qt,還是其他?
當一個程序員失去了對代碼的興趣,變得沒有目標沒有動力,是怎樣的體驗?
C# 作為一種靜態類型語言,為什麼會引入 var?
用慣了 C# 之後再也不想用別的語言了,正常嗎?

TAG:王垠人物 | C# |