- Visual C# 2008開發技術詳解
- 李容等編著
- 133字
- 2018-12-27 11:19:43
第6章 集合與泛型
集合與數組的基本功能大致相同,但它在處理數據時具有更強更靈活的功能。.NET Framework提供了用于數據存儲和檢索的專用集合類,包含在System.Collections和System.Collections.Generic(泛型)中。泛型是C# 2008的一個新增特性,通過泛型可以定義更安全的數據結構,得到更優質的代碼。在本章中,將對集合與泛型進行詳細介紹。
6.1 什么是集合
集合類是為保障數據的安全存儲和訪問設計的,常見的集合類如表6-1所示。
表6-1 常見的集合類

除了上述類外,還要重點說明一些接口。大多數集合類實現相同的接口,表6-2為一些集合類實現的接口。
表6-2 集合類的常見接口

以上知識點對于初學者而言可能比較難以理解,特別是像哈希表這樣的類。但是沒有關系,在下面的章節中,會對每個類都進行舉例介紹。ArrayList類在第5章已經詳細介紹過了,這里就不再贅述。下面就重點介紹其他類和相關接口。
6.2 SortedList可排序數組集合
SortedList集合的初始化方式有兩種,包括泛型的和非泛型的,如下所示。
SortedList sl = new SortedList(); //創建非泛型SortedList集合 SortedList<Tkey, Tvalue> sl = new SortedList<Tkey, Tvalue> (); //創建泛型 //SortedList集合
本節只介紹非泛型SortedList集合的用法,有關泛型將會在以后的章節中介紹。
打開VS2008,在D:\C#\ch6目錄下添加名為SortedListTest的控制臺應用程序,步驟如下所示。
(1)包含命名空間System.Collections,在Program.cs中添加如下代碼。
private static void KeyandValueOut(SortedList sl) { Console.WriteLine("\t-鍵-\t-值-"); for (int i = 0; i < sl.Count; i++) { Console.WriteLine("\t{0}:\t{1}", sl.GetKey(i), sl.GetByIndex(i)); //獲取每個元素的鍵和值 } }
(2)定義一個能將SortedList里元素的鍵和值輸出的方法,供Main()函數調用。在Main()函數中添加如下代碼。
SortedList sl = new SortedList(); //創建SortedList可排序數組 sl.Add(1, "you"); //向SortedList添加鍵和值 sl.Add(2, "me"); sl.Add(3, "him"); Console.WriteLine("SortedList里的元素共{0}個", sl.Count); //輸出SortedList的 //元素個數 Console.WriteLine("容量:"+sl.Capacity); KeyandValueOut(sl); //遍歷輸出SortedList所有的鍵及其對應的值 Console.ReadKey();
先實例化一個SortedList類,然后向里面添加三個元素,元素的鍵分別為“1”、“2”、“3”,元素的值分別為“you”、“me”、“him”,再輸出SortedList的元素個數及每個元素對應的鍵和值。運行結果如圖6-1所示。

圖6-1 運行結果
6.3 Queue消息隊列集合
同SortedList集合一樣,Queue隊列也有兩種初始化方式,如下所示。
Queue q = new Queue(); //創建非泛型隊列 Queue<T> q = new Queue<T>(); //泛型隊列
下面還是以例子說明非泛型Queue的用法。
打開VS2008,在D:\C#\ch6目錄下創建名為QueueTest的控制臺應用程序,在命名空間中包含System.Collections,然后在Main()函數中添加如下代碼。
Queue q = new Queue(); //創建隊列 q.Enqueue("I"); //給隊列添加元素 q.Enqueue("love"); q.Enqueue("peace"); q.Enqueue("! "); IEnumerator myEnumerator = q.GetEnumerator(); //實例化能循環訪問隊列枚舉數 //IEnumerator接口 Console.WriteLine("該隊列中的所有元素如下所示。"); while (myEnumerator.MoveNext()) //將枚舉數推到隊列的下一個元素 { Console.Write(myEnumerator.Current+""); //獲取隊列中的全部元素 } Console.WriteLine(); q.Peek(); //返回隊列的開始處 q.Dequeue(); Console.WriteLine("將某元素踢出隊列后剩余的元素如下所示。"); IEnumerator myEnumerator1 = q.GetEnumerator(); while (myEnumerator1.MoveNext()) { Console.Write(myEnumerator1.Current + " "); } Console.WriteLine(); if (q.Contains("love") == true) //查詢隊列中是否包含love元素 { Console.WriteLine("該隊列包含love元素"); } else { Console.WriteLine("該隊列不包含love元素"); } q.Clear(); //刪除隊列中的所有元素 Console.WriteLine("該隊列所含元素總數為:"+q.Count); Console.ReadKey();
運行結果如圖6-2所示。

圖6-2 運行結果
本例中,用到了隊列的一些常見操作。例如,向隊列添加元素,從隊列中刪除某元素,查詢某元素是否在隊列中,返回隊列的開始處,刪除隊列中的所有元素等。希望讀者能認真體會,下一節將會介紹另一個集合類——Stack棧集合。
6.4 Stack棧集合
和前兩種集合類一樣,Stack棧也有兩種初始化方式,非泛型和泛型。如下所示。
Stack st = new Stack(); //非泛型堆棧 Stack<T> st = new Stack<T>(); //泛型堆棧
同理,本節也舉例對Stack的用法進行說明。
打開VS2008,在D:\C#\ch6目錄下創建名為StackTest的控制臺應用程序。在命名空間中包含System.Collections,然后在Main()函數中添加如下代碼。
Stack st = new Stack(); //創建堆棧集合 st.Push(' a' ); //將a, b, c, d, e壓入棧 st.Push(' b' ); st.Push(' c' ); st.Push(' d' ); st.Push(' e' ); Console.WriteLine("堆棧中的元素共有:{0}", st.Count); IEnumerator myEnumerator = st.GetEnumerator(); //實例化能遍歷訪問堆棧中所有元素 //的IEnumerator接口 Console.WriteLine("堆棧中的所有元素如下所示。"); while (myEnumerator.MoveNext()) { Console.Write( myEnumerator.Current+" "); //把堆棧中所有元素輸出到控制臺 } st.Pop(); //將第一個元素彈出堆棧 IEnumerator myEnumerator1 = st.GetEnumerator(); Console.WriteLine(); Console.WriteLine("某元素彈出后堆棧中的剩余元素如下所示。"); while (myEnumerator1.MoveNext()) { Console.Write(myEnumerator1.Current + " "); } Console.WriteLine(); Console.ReadKey();
運行結果如圖6-3所示。

圖6-3 運行結果
本例主要介紹了對象的是入棧與出棧操作。從程序運行結果可以看出,Stack是表示對象后進先出的簡單結合。在程序中,依次壓入了“a”、“b”、“c”、“d”、“e”5個char型字符,讀者可以發現在堆棧中的排列是“e”、“d”、“c”、“b”、“a”,所以最后彈出堆棧中的第一個元素時,把“e”去掉了。下一節介紹最后一個集合HashTable,也就是常說的哈希表。
6.5 HashTable哈希表集合
與前面的三種集合不同的是,.NET Framework本身沒有對HashTable提供泛型,所以它的初始化方式只有一種,如下所示。
HashTable ht = new HashTable(); //創建哈希表集合
HashTable是本章的一個難點,可能會比前三種集合都難以理解。同SortedList集合一樣,HashTable也是鍵/值對的集合,但這些鍵/值是根據哈希代碼進行組織。還是先舉個例子對HashTable具體的用法進行說明。
打開VS2008,在D:\C#\ch6目錄下建立名為HashTableTest的控制臺應用程序。首先還是要包含System.Collections命名空間,在Program.cs中添加如下方法。
public static void PrintKeysAndValues(Hashtable myHT) { foreach (string s in myHT.Keys) { Console.WriteLine(s); Console.WriteLine(" -鍵- -值-"); } //HashTable中的每個元素都是鍵/值對,既不是鍵的類型,也不是值的類型,而是 //DictionaryEntry類型 foreach (DictionaryEntry de in myHT) { Console.WriteLine(" {0}: {1}", de.Key, de.Value); } Console.WriteLine(); }
上面的函數用作HashTable中所有元素的遍歷輸出,提供給Main()函數調用,接下來在Main()中添加如下代碼。
Hashtable ht = new Hashtable(); //創建HashTable ht.Add("Ea", "a"); //向HashTable中添加鍵/值對 ht.Add("Ab", "b"); ht.Add("Cc", "c"); ht.Add("Bd", "d"); Console.WriteLine("Hashtable表中的所有元素的鍵如下:"); PrintKeysAndValues(ht); //遍歷輸出HashTable中的所有鍵/值對 ArrayList al = new ArrayList(ht.Keys); al.Sort(); //將HashTable中的元素按鍵的首字母排序 foreach (string key in al) { Console.Write("鍵:"+key); //輸出鍵 Console.WriteLine("值:"+ht[key]); //輸出鍵對應的值 } Console.ReadKey();
運行結果如圖6-4所示。

圖6-4 運行結果
本例中,主要介紹了向HashTable中添加鍵/值對,并遍歷輸出其中的所有鍵/值對,最后對HashTable中的元素按鍵的首字母排序輸出。這里有如下兩個問題需要說明。
(1)從圖6-4中可以看出,HashTable元素的輸出順序并非是按添加的順序輸出。因為當某個元素被添加到HashTable中時,將根據鍵的哈希代碼進行組織存儲。哈希代碼比較復雜,讀者如果想深究,可參閱數據結構的相關書籍。
(2)當對HashTable中的元素進行遍歷輸出時,用到了結構體DictionaryEntry,這里有兩個原因。第一,HashTable不像SortedList那樣有訪問自身鍵/值對的方法;第二,HashTable中的每個元素都是鍵/值對,既不是鍵的類型,也不是值的類型,而是DictionaryEntry類型。至此,就介紹完了集合類中的幾個重要集合,下一節將介紹集合類的一些非常重要的概念。
6.6 集合中的一些重要概念
要很好地理解和運用集合,則必須理解集合中一些比較重要的概念,包括集合中的索引器、迭代器和深度復制等。
6.6.1 集合中的索引器
索引器在第3章曾經提到過,它在集合中應用比較廣泛。在集合中,索引器有兩個非常重要的特點。
(1)索引器有和數組相同的索引方式,但索引器可以不按照整數值進行索引,讀者可以自定義查詢機制,這是數組實現不了的。
(2)索引器可以被重載,也可以有多個參數。索引器在.NET Framework中沒有專有的名稱,它的定義形式如下。
T this[int pos]; //創建索引器
T表示返回類型,pos表示位置參數,因為它類似于屬性,所以它也能支持get和set操作。下面舉一個相關例子。
打開VS2008,在D:\C#\ch6目錄下創建名為IndexerTest的控制臺應用程序。首先,將Program.cs重命名為IntIndexer.cs,因為后面要用到該類的構造函數,這樣重命名方便于讀者理解。然后,在IntIndexer.cs中添加如下代碼。
string[] Data; public IntIndexer(int size) //索引器類構造函數 { Data = new string[size]; //定義一個字符串數組 for (int i=0; i < size; i++) { Data[i] = "no value"; //對數組中的每個元素進行賦值 } } public string this[int pos] //定義返回類型為string的索引指示器 { //get、set操作與屬性中的用法一致 get { return Data[pos]; } set { Data[pos] = value; } }
上面代碼在構造函數中對字符串數組Data進行初始化,并為其創建一個索引器,用get、set操作能對其中的元素進行獲取或設置。下面就是怎么調用的問題,在Main()函數中添加如下代碼。
int size = 10; IntIndexer Ind = new IntIndexer(size); Ind[2] = "Some Value"; Ind[3] = "Another Value"; Ind[7] = "Any Value"; Console.WriteLine("\n索引器輸出\n"); for (int i = 0; i < size; i++) { Console.WriteLine("Ind[{0}]: {1}", i, Ind[i]); } Console.ReadKey();
Main()函數的功能是初始化一個新的IntIndexer對象,并往里面添加一些新值,最后將全部元素輸出到控制臺。運行結果如圖6-5所示。

圖6-5 運行結果
6.6.2 集合中的迭代器
迭代器是C# 2008的一個重要功能,它可以是一種方法、get訪問器或是一種運算符,它支持在類或結構中實現foreach迭代,而不用實現整個IEnumerable接口。迭代器能返回具有相同類型的值的有序序列。使用yield return語句能依次返回所有元素,yield break能終止迭代操作。迭代器的返回類型必須是Ienumerable、Ienumerator或Ienumerable<T>、Ienumerator<T>。當執行到yield return語句時,能夠保存當前的位置。說了這么多理論,可能讀者也不知所措了,還是看看下面的例子,實現步驟如下。
(1)打開VS2008,在D:\C#\ch6目錄下建立名為IterateTest的控制臺應用程序。
(2)打開編譯器后,單擊“視圖”—“解決方案資源管理器”命令,在“解決方案資源管理器”窗口中右鍵單擊“IterateTest”按鈕,選擇“添加”—“新建項”命令,在彈出的窗口中選擇“類”選項,重命名為“MonthofYear.cs”,然后往里面添加如下代碼。
string[] m_Months = { "Jan", "Feb", "Mar", "Api", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; //通過迭代器實現System.Collections.IEnumerable接口的GetEnumerator()方法 public System.Collections.IEnumerator GetEnumerator() { for (int i = 0; i < m_Months.Length; i++) { yield return m_Months[i]; //依次輸出數組中的每個元素 } }
(3)通過GetEnumerator()方法返回一個枚舉器,實現數組元素的遍歷輸出。然后返回到Program.cs,在Main()函數中添加如下代碼。
MonthofYear my = new MonthofYear(); foreach (string Month in my) { System.Console.Write(Month + " "); } Console.ReadKey();
(4)程序運行結果如圖6-6所示。

圖6-6 程序運行結果
本例中,通過用迭代器實現System.Collections.IEnumerable接口的GetEnumerator()方法,完成了對字符串數組m_Months元素的遍歷輸出。重點說明的是迭代器本身不是成員,而是實現函數的一種手段。比如GetEnumerator()方法,如果它沒有被迭代器實現就不能被調用。
6.6.3 深度復制
在日常的應用中,常常會用到將一個變量的內容復制到另一個變量。這里有兩種方式,淺度復制和深度復制。下面先看看兩者的區別。如果是復制一個類的成員,那么淺度復制的新對象包含的引用對象和源對象的指向一致,也就是說,它既復制了值,又復制了值的引用;而深度復制的新對象與它復制的源對象是相互獨立的,它只復制了值,而沒有復制值的引用。
下面以一個例子來說明兩者的區別。
打開VS2008,在D:\C#\ch6目錄下建立名為ShadowCopyTest的控制臺應用程序。先添加一個新類Class1.cs(方法同上一節),并添加如下代碼。
public string val;
這里定義了一個字符串,用作被復制的源對象,然后在Program.cs中添加如下代碼。
Class1 cs = new Class1(); public Program(string nVal) { cs.val=nVal; } public object GetCopy() { return MemberwiseClone(); //創建源對象的淺復制副本 }
對Class1類進行實例化,并定義復制的方法。MemberwiseClone()派生于System.Object,能實現源對象的淺度復制。最后,在Main()函數中添加如下代碼。
Program pg = new Program("a"); //源對象 Program pg1 = (Program)pg.GetCopy(); //新對象 Console.WriteLine("源對象:"+pg.cs.val); Console.WriteLine("新對象:"+pg1.cs.val); pg.cs.val = "b"; //改變源對象 Console.WriteLine("改變后的源對象:"+pg.cs.val); Console.WriteLine("改變源對象后的新對象:" + pg1.cs.val); Console.ReadKey();
這段代碼主要是測試,改變源對象的同時復制了源對象的新對象是否發生改變。程序運行結果如圖6-7所示。

圖6-7 程序運行結果
從運行結果可以看出,新對象也跟著發生了變化。但有些時候,這不是需要的。通常需要新對象不隨著源對象發生改變,這就需要用到深度復制。接下來,將以上代碼中的GetCopy()方法修改為ICloneable里面的Clone()方法,該方法能實現深度復制,具體代碼如下。
public object Clone() { Program dpg = new Program(cs.val); return dpg; }
然后再將調用該方法的代碼修改如下。
Program pg1 = (Program)pg.Clone();
運行結果如圖6-8所示。

圖6-8 運行結果
從運行結果中可以看出,當源對象發生改變時,新對象沒有發生變化。深度復制較之與淺度復制并非有什么優勢,而是兩者應用的場合不一樣,這點讀者應該明白。至此,集合的幾個主要概念就介紹完畢,下一節將開始講述怎樣為集合使用泛型。
6.7 為集合使用泛型
泛型是C# 2008的重要特性,它的主要用途是定義類型安全的數據結構。“泛”,從字面意思講,應該是范圍廣,眾多的意思,“泛型”,應該解釋為范圍廣的類型,或者叫多類型。從這點上就可以看出它的一個優勢,就是能夠避免煩瑣的類型轉化。
6.7.1 定義泛型類
泛型在集合中會常常用到,例如,創建好一個集合類1,定義好它的類型和一些方法。但現在還需要一個集合類2,它和集合類1的區別僅是類型不一樣,但由此引發的一些方法也需要改變。如果不想再定義一個集合類2,就必須實現進行類型轉化或重載,但這也是很麻煩的。使用泛型能很好地解決這個問題。泛型類是在類的實例化過程中建立的,可以很容易實現強類型化,而且代碼很簡單。
看下面的一個簡單的例子,定義一個泛型隊列。
Queue<T> q = new Queue<T>();
泛型的使用分為兩種,一種是使用.NET Framework提供的泛型,另一種是使用自己創建的泛型。先介紹一些.NET Framework提供的泛型類和接口,它們包含在System.Collections. Generic命名空間中,如表6-3所示。
表6-3 常見的泛型類和接口

以上表格中的很多類或接口,似乎在前面已經見過,只是多了一個“<>”而已。對于非泛型集合和泛型集合的區別,想必讀者已經有了一個感性認識,下面通過一個簡單的例子進行說明。打開VS2008,在D:\C#\ch6目錄下建立名為GenericTest的控制臺應用程序,在Main()函數中添加如下代碼。
ArrayList al = new ArrayList(); al.Add(1); al.Add(2); al.Add(3.0); int total = 0; foreach (int val in al) { total = total + val; } Console.WriteLine("元素的總和為: {0}", total); Console.ReadKey();
本例是對ArrayList集合添加三個元素,并遍歷輸出,單擊F5鍵,程序能編譯通過,在運行期間拋出異常,如圖6-9所示。

圖6-9 程序運行異常
這是因為,代碼為ArrayList集合添加了三個元素,前兩個為int型,第三個為double型,而最后的遍歷輸出時指定的元素類型為int型,所以此時會拋出異常。對于這樣的小程序,出了異常可以立即進行改正。但對于一些數據結構十分復雜的大程序而言,程序員往往希望能在程序編譯過程中,就能找出潛在的錯誤。現在,將上面的代碼替換為如下所示代碼。
List<int> l = new List<int>(); l.Add(1); l.Add(2); l.Add(3.0); int total = 0; foreach (int val in l) { total = total + val; } Console.WriteLine("元素的總和為: {0}", total); Console.ReadKey();
按F5鍵,程序在編譯期間出現錯誤,如圖6-10所示。

圖6-10 編譯錯誤
本例中定義了一個泛型集合,通過類型指定,在編譯期間即能檢測出錯誤。這很好地體現了泛型能定義類型安全的數據結構。除了能定義泛型類外,還能定義泛型接口、泛型委托、泛型方法等,在下面的章節中將作詳細介紹。
6.7.2 定義泛型接口
為泛型集合類定義泛型接口是非常重要的,它可以避免進行值類型的裝箱與拆箱操作,使代碼更簡單,運行速度更快。如果將接口指定成了某類型的約束時,就必須使用實現該接口的類型。.NET Framework中有一些比較重要的泛型接口,參見表6-3。泛型接口的定義語法和泛型類比較相似,看下面的代碼。
interface myinterface<T> where T : Class1
上面代碼定義了一個泛型接口myinterface<>, interface是接口的關鍵字。有關泛型接口的舉例會在后面的章節講到。
6.7.3 定義泛型方法
泛型方法的定義也很簡單,如下面的代碼。
public T Method<T>() { return default(T); }
上面代碼定義了一個名為Method的泛型方法,讀者需要注意的是該方法的返回類型和參數都必須使用泛型類型參數。使用default關鍵字是為了為類型T返回默認值。定義的方法自然應該被調用,下面接著看怎樣調用該方法,代碼如下。
string result = Method<T>(); //調用泛型方法
在調用該泛型方法時,指定了方法的返回類型為string型。泛型方法的舉例也放在后面講。
6.7.4 定義泛型委托
在前面章節中介紹了委托的定義,下面先看看定義一般委托與泛型委托的區別。
public delegate int mydelegate(int p1, int p2) //一般委托 public delegate T1 mydelegate<T1, T2>(T2 p1, T2 p2)where T1:T2 //泛型委托
一般委托能夠調用方法,這在前面章節中已經講過。泛型委托調用方法的方式和一般委托差不多,看下面的例子。
public delegate void mydelegate<T>(T item); public static void Method(int i) { } mydelegate<int> md = new mydelegate<int>(Method);
本例中,定義了一個帶有int型參數的Method方法。使用泛型委托mydelegate進行調用,在實例化過程中為Method方法指定了參數類型。至此,泛型的基本類型就介紹完了。
6.8 小結
本章主要介紹了一些常見的集合類,如SortedList集合、Queue集合、Stack集合和HashTable集合等。同時分別舉例說明了它們的用法,如怎樣添加元素,怎樣遍歷訪問等。然后,說明了集合中的幾個重要概念,即索引器、迭代器和深度復制,并舉例說明了它們的用途。在本章最后,引入C# 2008的一個重要特性——泛型。分別介紹了怎樣定義泛型類、泛型接口、泛型方法、泛型委托等,最后以一個自定義的泛型鏈表說明了泛型的應用。