Effective C# 根據需要選用恰當的集合

如果要問 “哪種集合是最好的?”我的回答是:“視需要而定。”不同的集合有不同的功能特性,並且針對其行爲的不同進行了優化。.Net Framework支持許多相似的集合:列表、數組、隊列、棧等等。另外,C#支持多維數組,其性能特點不同於其它的一維數組或者交錯數組。.Net Framework中還包含了很多專門化的集合,你可以回顧一下以前創建的程序中用到的那些集合。由於所有的集合都實現了ICollection接口,你可以非常快速的找到它們。在描述ICollection接口的文檔中列出了所有實現這個接口的類。這二十多個類都是可供我們使用的集合。

      在創建集合時,你應當考慮最經常對這個集合執行哪些操作,這有助於選出適合你需要的正確的集合。另外,爲了使程序更具彈性,你應當依賴於集合類所實現的接口編程,這樣即使發現當初設想中使用的集合是不正確的,你仍然可以用其它的集合來代替它。

      在.Net Framework中有三種不同類型的集合:數組、類數組集合和基於哈希原理的集合容器。其中數組是最簡單,一般來說也是速度最快的,那就讓我們先從這裏說起吧。數組是我們最常用的集合類型。

      一般來說當需要使用集合時,System.Array類,或者更恰當的說,一個指定類型的數組類應當是你的第一選擇。選用數組最重要的原因就是數組是類型安全的。除C# 2.0中的泛型(參見本書第49項)外,其它集合儲存的都是System.Object類型的引用。當我們聲明數組時,編譯器會爲我們指定的類型創建一個特殊的System.Array的派生。例如下例中的聲明將創建一個整型數組:

private int [] _numbers = new int[100];

      數組中儲存的將是整數,而不是System.Object。這樣的意義在於當我們添加、獲取或者移除數組中的值類型的時候,可以避免裝箱和拆箱操作所帶來的效率上的損失(參見本書第17項)。上例中的初始化過程創建了一個可以儲存100個整數的一維數組。數組所佔用的內存單元都被置以0。值類型數組的初始值都是0,而引用類型數組的初始值都是null。我們可以通過索引來訪問數組中的每一項。

int j = _numbers[9];

  除此之外,我們還可以使用foreach或者枚舉器來遍歷數組

  foreach (int i in _numbers)
  
{
      Console.WriteLine(i.ToString());
  }

            
//或者


        IEnumerator it 
= _numbers.GetEnumerator();
        
while (it.MoveNext())
          
{
                
int i = (int)it.Current;
                Console.WriteLine(i.ToString());
            }

 

 

如果你需要儲存單一序列的對象,你應當選擇數組來儲存他們。但是一般來說,我們的數據構成都是比較複雜的集合。這很容易讓我們馬上倒退回C語言風格轉而使用交錯數組――一種包含數組的數組。有時這正是我們需要的。在交錯數組中外層集合的每個元素都是一個數組。

 

 

    public class MyClass
    
{
        
private int[][] _jagged;
        
public MyClass()
        
{
            _jagged 
= new int[5][];
            _jagged[
0= new int[10];
            _jagged[
1= new int[12];
            _jagged[
2= new int[7];
            _jagged[
3= new int[23];
            _jagged[
4= new int[5];
        }

    }

 

外層數組內部存儲的每個一維數組可以是不同大小的。當需要創建不同大小的數組的數組時,你可以使用交錯數組。交錯數組的缺點在於列方向遍歷的效率低。例如現在要檢查交錯數組中每一行第三列的值,每檢查一行,都需要對數組進行2次查找。在交錯數組中,第0行第3列的元素和第1行第3列的元素之間並沒有關聯關係。只有多維數組才能高效的完成列方向上的遍歷。以前C和C++的程序員使用一維數組來完成對將二維(或多維)數組的映射。對於以前的C和C++程序員來說,這樣的代碼是很清晰的:

double num = MyArray[ i * rowLength + j ];


      而其它人更喜歡這樣寫:

double num = MyArray[ i, j ];

      但是C和C++不支持多維數組,而C#支持。使用多維數組可以創建一個真實的多維結構,不論對於你還是編譯器來說都會更加清晰。你可以使用類似於一維數組聲明的標記來創建一個多維數組。

private int[,] _multi = new int[10, 10];


      上面的聲明創建了一個二維數組,10×10的陣列共100個元素。在多維數組中每一個維度的長度都是恆定值。利用這個特性,編譯器可以生成高效的初始化代碼。而初始化一個交錯數則組需要多次初始化聲明。在早些的簡單例子中可以看到,對於例子中的交錯數組,你需要聲明五次。交錯數組越大、維數越多所需要的初始化代碼也越龐大,你必須手工來完成這一切。然而對於多維數組來說,所需要的僅僅是在初始化聲明時指定其維度。此外,多維數組還可以高效的初始化數組元素。對於值類型的數組來說,有效範圍內的每個索引所對應的元素,都被初始化爲一個值的容器。這些值的內容都是0。引用類型的數組的每個索引對應的都是null。對於數組的數組,其存儲單元內部也是null。

      一般來說,多維數組中的遍歷要比交錯數組快的多,特別是列方向或斜線方向的遍歷。編譯器可以使用指針算法來處理數組中的任意一個維度。而對於交錯數組來說,這需要在每個一維數組中搜索正確的值。

      多維數組可以充當任意的集合,在很多場合都能發揮作用。假設你要創建一個在棋盤上進行的遊戲。你需要安排一個有64塊區域的表格來做爲棋盤:

private Square[,] _theBoard = new Square[8, 8];

      這樣的初始化方式創建了儲存這些Square類型的數組。假設Square是引用類型,由於這些Square類型本身還沒有被創建,因此每個數組中存儲的元素都是null。爲了初始化這些元素,我們必須考慮到數組中的每一個維度。

 

for (int i = 0; i < _theBoard.GetLength(0); i++)
  
{
        
for (int j = 0; j < _theBoard.GetLength(1); j++)
        
{
            _theBoard[i, j] 
= new Square();
        }

    }

 

但是在多維數組中,你擁有更加靈活的遍歷元素方式。我們可以通過數組的索引來獲取其中合法的元素:

Square sq = _theBoard[4, 4];


      如果你需要遍歷整個集合,你可以使用迭代器

 

foreach(Square sq in _theBoard)
{
    sq.PaintSquare();
}



與之對比的是如果我們使用交錯數組:

foreach(Square[] row in _theBoard)
{
    foreach(Square sq in row)
    {
        sq.PaintSquare();
    }
}



交錯數組中增加每一個新的維度代表着需要聲明一個新的foreach來完成遍歷。而在多維數組中,一個foreach聲明就可以生成檢查每個維度是否越界和獲取數組中元素的所有代碼。foreach聲明會生成特殊的代碼來對數組的每個維度進行遍歷。foreach循環所生成的代碼等同於如下代碼:

        for (int i = _theBoard.GetLowerBound(0); i < _theBoard.GetUpperBound(0); i++)
            {
                for (int j = _theBoard.GetLowerBound(1); j < _theBoard.GetUpperBound(1); j++)
                {
                    _theBoard[i, j].PaintSquare();
                }
            }



這些代碼看起來效率並不高,因爲在循環內部調用了GetLowerBound和GetUpperBound方法,但是實際上這是最高效的結構。JIT編譯器可以將數組的邊界緩存起來,並且取消內部對數組越界判斷的操作。

      數組類有兩個主要的缺點,正是這兩個缺點使得.Net Framework中其它的集合類型有了用武之地。第一個缺點影響數組的大小調整:數組不能動態的調整大小。如果你需要調整數組某一維度的大小,你就必須重新創建一個數組並從原數組中將所有已存在的元素拷貝至新數組。調整大小非常耗時:一個新的數組必須被分配空間,已有數組中的全部元素必須被拷貝到新數組中。儘管這種在託管堆上的拷貝和移動的代價已經不像C或者C++時代那樣昂貴,但是依然會耗費時間。而更重要的是這種操作可能導致陳舊數據被應用。考慮下面的代碼片斷:

 

   private string[] _cities = new string[100];

        
public void SetDataSource()
        
{
            myListBox.DataSource 
= _cities;
        }


        
public void AddCity(string cityName)
        
{
            
string[] temp = new string[_cities.Length + 1];
            _cities.CopyTo(temp, 
0);
            temp[_cities.Length] 
= cityName;
            _cities 
= temp;
        }

 

即便是AddCity方法被調用之後,列表框所使用的數據源仍然是_cities數組的老版本拷貝。新添加的城市永遠不會顯示在列表框之中。

      ArrayList類是構建在數組上的一種高層次抽象。ArrayList集合混合了一維數組和鏈表的特徵。你可以在ArrayList中進行插入操作,也可以調整它的大小。ArrayList將其大部分職責都委託給其內部包含的數組,這意味着ArrayList類在功能特性上和Array類是非常相似的。當我們可以使用ArrayList來輕鬆的應對未知大小的集合,這也是ArrayList較Array而言的主要優點。ArrayList可以隨時增長或縮減。雖然我們仍然需要付出拷貝和移動數組元素的代價,但是這些算法的代碼是已經寫好並經過測試的。由於ArrayList對象內部儲存數據的數組是封裝好的,也不會出現陳舊數據的問題:客戶程序將指向ArrayList對象而不是內部數組。ArrayList集合是C++標準類庫中的vector類在.Net Framework中的版本。

      隊列和棧類在System.Array基礎上提供了專門的接口。通過這些類的特定的接口實現了先進先出的隊列和後進先出的棧。我們要始終牢記這些集合是使用其內部的一維數組來儲存數據的。當我們改變它們的大小時同樣會受到性能上的損失。

      .Net中不包含鏈表結構的集合。由於有高效的垃圾收集機制,表結構出場亮相的次數也減少了。如果你的確需要實現鏈表行爲時,你有兩種選擇。如果你引起經常要添加或移除項目而使用列表時,你可以使用字典類簡單的儲存鍵,對於值則賦以null。當需要實現一個鍵/值的單鏈表時,你可以使用ListDictionary類。或者你可以使用HyBridDictionary類。當集合較小時,HyBridDictionary類會使用ListDictionary來應對,而對於較大的集合則選用HashTable。這幾個集合和其它許多集合一起位於System.Collections.Specialized命名空間下。儘管如此,如果你爲了實現某些用戶指令的目的而使用鏈表結構的話,那麼你完全可以使用ArrayList集合來代替它。儘管ArrayList內部是使用數組來進行存儲的,但是它也可以完成在任意位置插入元素的功能。

 

 

另外兩種支持基於字典的集合是SortedList和Hashtable。它們都包含鍵/值對。SortedList會對鍵進行排序而Hashtable不會。Hashtable提供了對給定鍵的快速搜索,而SortedList提供了按鍵的順序遍歷元素的功能。Hashtable通過做爲鍵的對象的哈希值來進行搜索,如果哈希鍵是足夠高效的話,那麼其每次搜索操作所耗費的時間是一個常數,即時間複雜度爲0(1)。SortedList使用二分法來進行搜索,這種算法操作的時間複雜度爲0(ln n)。

      最後我們來介紹一下BitArray類。顧名思義,這個類是用來存儲二進制數據的。BitArray類使用一個整型的數組來儲存數據。整型數組中的每個存儲單元儲存32個二進制值。這樣做可以達到壓縮的目的,但是同樣也會降低性能。每次對BitArray進行get或者set操作都會引發對儲存着目標數據和其它31個二進制數據的整數的操作。BitArray包含了一些方法來對其內部的值進行布爾型操作,例如:OR,XOR,AND和NOT。這些方法使用BitArray做爲參數,可以被用來快速過濾BitArray中的多位二進制數。BitArray針對位操作做了專門的優化,應當使用它來存儲那些經常進行做爲掩碼的二進制標記集合,而不應當使用一般的布爾型的數組來代替。

      除了Array類之外,在.Net Framework 1.x版本的C#中再也沒有其它集合類是強類型的。它們儲存的都是Object的引用。C#泛型中包含了一種新版本的拓撲結構,它能夠以一種更加普遍化的方式被創建。泛型是創建類型安全集合的最好方法。你也可以通過現在的System.Collection命名空間中包含的抽象基類在非類型安全的集合上構建你自己的類型安全接口:CollectionBase和ReadOnlyCollectionBase提供了存儲鍵/值集合的基類。DictionaryBase類使用的是哈希表的實現方法,他的功能特點和哈希表非常相似。

      當你的類包含集合時,你會希望爲將它暴露給你的類用戶。你有兩種方法來達到這個目的:使用索引器或者實現IEnumerable接口。在本節開始的部分,我向你展示了數組如何通過[]標記來獲取其中的項目,你也可以使用foreach來遍歷數組中的項目。

      你可以爲你的類創建多維索引器。這很類似於C++中重載操作符[]一樣。就像C#中的數組一樣,你可以創建多維的索引器:

 
    public int this[int x, int y]
      {
            get
            {
                return ComputeValue(x, y);
            }
        }


添加索引功能通常意味着你的類型內部包含一個集合。而這也意味這你的類型應當支持IEnumerable接口。IEnumerable接口提供了一種標準的迭代遍歷集合中所有元素的機制。

 
public interface IEnumerable
{
        IEnumerator GetEnumerator();
}

      //GetEnumerator方法返回一個實現了IEnumerator接口的對象。IEnumerator接口支持對集合的遍歷:

public interface IEnumerator
{
        object Current { get;}
        bool MoveNext();
        void Reset();
    }


除了IEnumerable接口外,如果你的類型要模擬一個數組,那麼你還應當考慮IList和ICollection接口。如果你的類型要模擬一個字典,那麼你應當考慮實現IDictionary接口。當然,你可以自己來實現這些龐大的接口,如果要解釋實現方法的話,我恐怕需要多花上許多篇幅。其實有一個更簡單的解決辦法:當我們要創建特殊目的的集合時,我們可以從CollectionBase或者DictionaryBase來派生出我們的類。

      讓我們來回顧一下本節所覆蓋的內容。一個最好的集合取決於它要執行的操作和應用程序對空間和時間的要求。在大多數情況下,Array類提供了最高效的集合容器。C#中多維數組的出現意味着我們可以非常簡單的模擬多維結構而不必擔心犧牲性能。當你的程序需要更加靈活的添加和刪除項時,你可以哪些使用更加靈活的集合類型。最後,當你要創建一個模擬集合的類時,應當爲其實現索引器和IEnumerable接口。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章