【干貨】對IDisposable和靜態分析的提議:DisposeUnused屬性
當 .NET初創的時候,關于IDisposable該如何使用存在一定的不確定性。結果,IDisposable的應用方式過于激進,許多種類的類都需要空的Dispose方法。這給靜態分析工具帶來了一些問題,它們無法將實際缺少Dispose調用與誤報區分開來。
為了理解始末緣由,我們需要回過頭看一下CLR早期的歷史以及垃圾收集是如何運行的。最初,CLR的目的在于是作為Visual Basic的新運行時,在20世紀90年代末Visual Basic是基于COM的。在COM模型下,對象會有一個引用計數。在引用創建和銷毀的時候,引用計數會隨之進行更新,如果計數變成零,對象就會被釋放。這樣的話,就形成了一個具有確定性的垃圾收集模型,在這種模型中,我們可以確切地知道該在什么時候清理資源。
引用方式的垃圾收集模型的最明顯缺點就在于它很容易出現內存泄露。如果我們創建了一系列的對象,它們之間互相循環引用的話,每個對象都會讓其他對象的引用計數無法降低到零,從而會出現內存泄露。在多線程環境中,它還會產生性能問題,因為在調整引用計數的時候,需要用到鎖。
在開發的早期,微軟決定采用標記-清理(mark-and-sweep)垃圾收集器來讓CLR避免這些問題。隨著Java的流行,這種方式在社區中得到了普遍的認可。但是,這種方式的GC并不能確定地釋放資源,這使得它不適合用于數據庫連接、文件處理和其他高度受限的資源。因此,IDisposable應運而生。
與此同時,微軟正在試驗“組件”的理念。組件的概念從來沒有被很好地定義。在這方面,有Component類,以及像IComponent、IContainer和ISite這樣的接口。在將近20年之后,文檔中也只有一個很模糊的注釋:“應用程序之間的對象共享”。其想法大概和COM類似,也就是某個應用可以直接與其他程序進行交互。但是,這并沒有達到目的,所以被埋沒在歷史的故紙堆中了。
而在Windows Forms中,有一個不同的“組件”概念,它真正的意思是“可以放到表單/窗口(form/window)中的內容”。除了像文本框這樣的實際UI元素之外,還包括添加了特定功能的對象,如計時器。這來自VB 6編程時代,當時幾乎所有想使用的內容都必須要放到表單中。甚至數據庫連接和命令也可以直接放到表單中。
這也就是為什么我們看到很多毫不相關的對象也標記成了IDisposable。像DataTable和SqlCommand并沒有要處理的非托管資源,但是在過去我們錯誤地認為最好將它們放到表單中,所以它們繼承了Component類。而Component是disposable的,所以我們可以選擇何時關閉代理對象。
靜態分析
隨著靜態分析逐漸從高級工具變成了每個開發人員都該使用的工具,關于disposable對象不斷增長的告警越來越成為一個問題。對于像SqlCommand這樣短期存活的對象來說,這還不算太糟糕,因為可以很容易地使用using語句對其進行包裝,而不需要考慮該語句實際上并沒有執行任何操作。
像DataTable這樣的類就比較困難了。這是一個長期存活的對象,它所使用的地方可能距離創建它的地方非常遠。除非設置為suppressed或禁用,否則靜態分析工具將會報告DataTable和類似對象有未處理處理的告警和錯誤。
DisposeUnusedAttribute提議
“最佳”方案是徹底移除所有無用的Dispose方法。但是,這并不是可行方案,因為這樣會破壞向后的兼容性。
Edward Brey提出了一個相當簡單而優雅的解決方案。他建議創建一個DisposeUnused屬性來屏蔽靜態分析工具。子類不會繼承此屬性。
但是,這個設計也并非盡善盡美。一旦DisposeUnused用到了某個類上,移除它將會是破壞性的變更。對于DataTable來說,這并不是什么問題,不過,Stephen A. Imhoff提供一個這樣的樣例。
這實際上會更糟糕,因為現在你可能會說“好的,忽略該契約,我就是這樣聲明的”,如果某個類型突然需要dispose某個資源的話(比如說,MemoryStream要為大型數組或其他內容分配一個原生數組),那么你的消費者需要執行dispose操作,但是你之前卻告訴人家不需要這樣做……
另一個問題是你并不是總能知道變量里有什么。假設有一個類型為Component的變量。在編譯時,我們無法判斷放入變量的內容是否需要dispose處理。因此,更有意義的做法是只將DisposeUnused用到密閉類(sealed classed)中,這些類是無法子類化的。