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

來源:互聯網
上載者:User
如果要問 “哪種集合是最好的?”我的回答是:“視需要而定。”不同的集合有不同的功能特性,並且針對其行為的不同進行了最佳化。.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介面。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.