前言
軟體開發過程中,不可避免會用到集合,C#中的集合表現為數組和若幹集合類。不管是數組還是集合類,它們都有各自的優缺點。如何使用好集合是我們在開發過程中必須掌握的技巧。不要小看這些技巧,一旦在開發中使用了錯誤的集合或針對集合的方法,應用程式将會背離你的預想而運作。
建議16、元素數量可變的情況下不應使用數組
建議17、在多數情況下使用foreach進行循環周遊
建議18、foreach不能代替for
建議19、使用更有效的對象和集合初始化
建議16、元素數量可變的情況下不應使用數組
在C#中,數組一旦被建立,長度就不能改變。如果我們需要一個動态且可變長度的集合,就應該使用ArrayList或List<T>來建立。而數組本身,尤其是一維數組,在遇到要求高效率的算法時,則會專門被優化以提升其效率。一維數組也成為向量,其性能是最佳的,在IL中使用了專門的指令來處理它們。
從記憶體使用的角度來講,數組具有以下特點:
1、數組在建立時被配置設定了一段固定長度的記憶體。
2、如果數組元素是值類型,則每個元素的長度等于相應的值類型的長度
3、如果數組的元素是引用類型,則每個元素的長度為該引用類型的IntPtr.Size。
4、數組的存儲結構一旦被配置設定,就不能再變化。
而ArryaList是這樣的:
1、ArrayList是連結清單結構,可以動态增減記憶體空間。
2、如果ArrayList存儲的是值類型,則會為每個元素增加12位元組的空間,其中4位元組用于對象引用,8位元組是元素裝箱時引入的對象頭。
而List<T>是ArrayList的泛型實作,它省去了拆箱和裝箱帶來的開銷。
如果一定要動态改變數組的長度,一種方法是将數組轉換為ArrayList或List<T>,如下面的代碼所示:
還有一種方法是用數組的複制功能。數組繼承自System.Array,抽象類System.Array提供了一些有用的實作方法,其中就包含了Copy方法,它負責将一個數組的内容複制到另外一個數組中。無論是哪種方法,改變數組長度就相當于重新建立了一個數組對象。
為了讓數組看上去本身就具有動态改變長度的功能,還可以建立一個名為ReSize的擴充方法。
調用方式如下:
下面我們來對比一下性能,先來看代碼:
Main函數中主要是調用,自己定義的兩個方法,第一個是重新設定數組的長度,第二個是設定List<T>的長度,通過運作時間進行測量:
嚴格意義上講,List<T>不存在改變長度的說法,此處主要是來進行對比一下,對List<T>設定長度,并且進行指派,即便是這樣,在時間效率上ResizeList比ResizeArray要高很多很多。
建議17、在多數情況下使用foreach進行循環周遊
感覺使用foreach進行循環周遊,總共有三個好處吧:
1、提供了比較簡單、簡潔的文法。
2、自動将代碼置入try-finally塊
3、若類型實作IDispose接口,foreach會在循環結束後自動調用Dispose方法
建議18、foreach不能代替for
foreach存在一個問題是:它不支援循環時對集合進行增删操作。我們來看一下簡單的例子:
一起看一下執行結果:
那麼下面我們來使用for進行嘗試:
進行删除肯定是沒問題的。但是要仔細看一下,比如它第一次删除索引0的時候,也就是删除了1,那麼它會立即重新調整索引,然後第二次删除的時候,删除的不是2,而是3這個項。那麼最終運作完發現還剩餘兩項
foreach循環使用了疊代器進行集合的周遊,它在FCL提供的疊代器内部維護了一個對集合版本的控制。那麼什麼是集合版本呢?簡單的說,其實它就是一個整型的變量,任何對集合的增删操作都會使版本号加1。foreach循環會調用MoveNext方法來周遊元素,在MoveNext方法内部會進行版本号的檢測,一旦檢測到版本号有變動,就會抛出InvalidOperationException異常。
如果使用for循環就不會帶來這樣的問題。for直接使用所引器,它不對集合版本号進行判斷,是以不存在因為集合的變動而帶來的異常(當然,超出索引長度這種情況除外)。
索引,因為版本檢測的緣故,foreach循環并不能帶起for循環。
建議19、使用更有效的對象和集合初始化
對象初始化設定項支援可以直接在大括号中對自動實作的屬性進行指派。
以往隻能依靠構造方法傳值進去,或者在對象構造完畢後對屬性進行指派。現在這些步驟簡化了,初始化設定項實際相當于編譯器在對象生成後對屬性進行了指派。
使用集合的初始化設定項,編譯器會在集合對象建立完畢後對集合調用Add方法。上面這段代碼展示了如何在初始化語句中建立一個新對象或一個現有對象,以及一個null值。
不過,初始化設定項絕不僅僅是為了對象和集合初始化的友善,它更重要的作用是為LINQ查詢中的匿名類型進行屬性的初始化。由于LINQ查詢傳回的集合中匿名類型的屬性都是隻讀的,如果需要為匿名類型屬性指派,或者增加屬性,隻能通過初始化設定項來進行。初始化設定項還能為屬性使用表達式。
來看一段代碼:
AgeScope 屬性是經過計算得出的,有了如此友善的初始化方式,使得代碼更加優雅靈活。