ArrayList是以前最常用的一個Collection. <<.net設計規範>>第八章一條: 要優先使用集合,而不是優先使用數組. 因ArrayList比Array好用多了:)
記得以前面試的時候主考官問我用過ArrayList沒,答:用過; 問:與Array有什麼區別.答:這個...這個...記不起來了.事後坐公交的時候,突然想到ArrayList不就是Array的List,數組鏈表嗎.狂拍自己腦袋.所幸主考官還是讓我pass了,有幸進入到現在的公司.
用反射看了一下ArrayList,其實就是對一個Array的封裝.就如<<Maximizing .Net Performance>>: "The only collection implemented directly at a runtime level is System.Array, and all other collection types are implemented using Array." ArrayList底層用的資料對象就是一個Object的 Array.只不過它提供了一個List功能的介面.初始化預設的大小是4,以前咋聽說是16呢.
當我們調用其Add()方法時,首先會去判斷一下是否有足夠的空間 if (this._size == this._items.Length), 不夠的話就調用EnsureCapacity()方法,確保其已指派足夠的空間.在申請新的一片記憶體空間時,會調用Array.Copy()方法將原有的資料複製到新的地址.(哈哈,跟我以前大學學C#做過的一個練習,實現的方式是一樣的).這個不僅讓我想到了一個效能問題.資料結構裡面的Array的List模型可非如此.應該是當申請完一片新的空間後,原來舊空間的最後一個位置通過指標連結到新空間的首地址.這樣的話就不需要拷貝原來資料到新的空間地址裡了.而且資料可以儲存在不連貫的空間上.更加合理地利用了空間.當然這樣的實現會更複雜些,帶來的最大好處就是頻繁擴大空間時不需要拷貝一次舊的資料.當然通過索引來訪問資料,可能沒有用一個數組那樣訪問那麼快了.
所以說在ArrayList初始化的時候,應該給它一個初始的Capacity(如果大致知道需要容器的大小,或者在需要很大的情況下,盡量稍微分大一點),盡量避免多次重新分配空間,避免頻繁拷貝資料帶來的開銷.
接著說說ArrayList.InsertRange(int index, ICollection c) 和 List<T>.InsertRange(int index, IEnumerable<T> collection).仔細觀察一下兩個方法,它們第二個參數的類型是不一樣的.ICollection是繼承自IEnumerable的.這個改變體現了<<.net設計規範>>第八章一條: 要用最範的類型作為參數類型.另一條是: 避免使用ICollection<T>或ICollection來做參數,如果其目的只是為了訪問該介面的Count屬性.還舉了個例子,說 可以考慮用IEnumerabler作為參數,然後動態地檢查一下對象,看它有沒有實現ICollection.
在此也有個概念可以得到明確:實現了IEnumerabler介面的類就是一個集合,不要以為ICollection介面上有個Collection就認為集合一定要實現此介面.(不過一般都會實現此介面 :))
再說List<T>.InsertRange()方法,因為是 IEnumerable<T>型別參數,我們拿不到它的Count,於是會先將其轉型為ICollection<T>,看是否成功,成功的話就可以只需調用一次EnsureCapacity(),一次性的分配足夠的空間就行了.然後調用ICollection<T>.CopyTo()方法一次性Copy就好了.如果不是,那隻能逐個逐個遍曆再Insert了,效能肯定是要損失了.不過貌似看到的集合類都是直接或間接實現了ICollection或ICollection<T>介面.
這裡讓我想到了前幾天跟Leader的一個關於設計的爭論.焦點在於他說這種向下轉型(如IEnumerabler --> ICollection)代碼不優雅,而需要設計出一個輔助類去獲得一個真正我們想要的子類型.而我覺得這樣添加一些額外的操作卻根本沒帶來什麼好處,向下轉型本身就很簡潔,很平常的事.理由就是: 在設計架構的時候它為了實現通訊必須提供它自己的契約:一組介面,而我們在應用的時候需要去擴充我們自己的功能,於是在實現了架構的契約後會提供一組自己的介面.但同時架構在通訊的時候只知道自己定的契約,而不知道我們的類,我們擴充的功能,所以有時候向下轉型是避免不了,也是不為過的.
再看List<T>.InsertRange()方法,因為它定義的參數類型是IEnumerable<T>,那麼它就可以接受更寬泛的參數來源.然而在其內部實現的時候,雖然可以完全用IEnumerable<T>操作,遍曆它然後一個一個調用Insert.但這樣的效能肯定是要被人唾棄的.一個簡單的向下轉型,轉為ICollection<T>就能帶來效能的提升,何樂而不為呢.
我現在就可以更理直氣壯地說,瞧FCL裡面實現也是避免不了向下轉型,你能說這樣一定就不優雅了?
下一片繼續探討一下同步的ArrayList,和一個搞笑的GetRange()方法.