.NET提供了一級功能強大的集合類,實現了多種不同類型的集合,可以根據實際用途選擇恰當的集合類型。
除了數組 Array 類定義在System 命名空間中外,其他的集合類都定義在System.Collections 命名空間中。為了方便、快捷地操縱集合元素,.NET 專門為集合定義了一套接口,.NET 中的集合類都實現了一個或多個接口,並且每個集合類都擁有適合自身特點的獨有方法,因此可以非常方便的操控集合中的元素。
為何對於集合類元素,可以使用foreach方法進行遍歷,這是因所有的集合都直接或間接地實現了IEnumerable接口,這個接口定義了GetIEnumerator()方法,該方法返回了一個IEnumerator對象稱為為迭代器,foreach語句就是通過訪問迭代器而獲取集合中元素的。
public interface IEnumerator
{ bool MoveNext(); //獲取下一個元素
object Current{get;} //獲取當前元素
void Reset(); //將枚舉數設置為其初始位置 }
ICollection接口也是一個很基礎的接口,它繼承於IEnumerable,並且添加了一個CopyTo()方法,一個Count屬性以及兩個用於同步的屬性。
public interface ICollection : IEnumerable
{ void CopyTo(System.Array array, int index); //復制到數組
int Count { get; } //元素個數
bool IsSynchronized { get; } //是否同步
object SyncRoot { get; } //用於同步的對象 }
IList 接口和IDictionary 接口都繼承於ICollection 接口。
Array類,C#為創建數組提供了專門的語法。數組有一個非常顯著的優點,即可以根據下標高效地訪問數組的元素;但是數組也有一個缺點,即在創建時數組的大小就已經確定了,不能動態地增加元素。Array 類有許多有用的方法,例如靜態方法Sort()方法和Copy()方法,Sort()方法的功能是對數組排序,Copy()方法的功能是復制數組,它有三個參數,第一個參數是原數組,第二個參數是目標數組,第三個參數表示復制元素的個數。
1.泛型
泛型是C#2.0 推出的一個新特性,通過它可以定義像文檔模板一樣的類模板。
(1)泛型的概念
在 ArrayList、Stack、Queue 等集合類中,元素的類型均為Object。由於.NET 中所有類都繼承於Object,所以這些集合可以存儲任何類型的元素。使用這些集合添加元素和讀取元素時分別需要裝箱和拆箱操作,例如:
ArrayList list = new ArrayList();
list.Add(10); //添加元素
int n = (int)list[0];
這樣做有三個缺點:
第一,在裝箱、拆箱的過程中,可能會造成一定的性能損失。
第二,無論什么樣的數據都能往集合里放,不利於編譯器進行嚴格的類型安全檢查。
第三,顯式轉換會降低程序的可讀性。
為解決這些問題,C#2.0推出了泛型(Generic),泛型集合類和非泛型集合類功能上基本一致,唯一的區別是泛型集合類的元素類型不是Object,而是自己指定的類型。
例如可以自行定義一個隊列泛型類public class MyQueue<T>{},,泛型類與普通類的區別是它具有一個類型參數列表<T>,在整個類里用T 代表元素類型。在定義類的時候,元素類型是未知的,是一個抽象的類型。在使用泛型類時,應為抽象類型T 指定具體類型。例如:
//使用時用int 替換抽象類型T
MyQueue<int> q1 = new MyQueue<int>(20);
//用char 替換抽象類型T
MyQueue<char> q2 = new MyQueue<char>(20);
用具體值類型int代替抽象類型T時,公共語言運行時在進行JIT 編譯時,會用int 代替泛型類中的T,創造出一個以int 為元素類型的新類,這個過程稱為泛型類型的實例化(Generic Type Instantiation)。不同的值類型,會實例化出不同的新類。但對於所有引用類型共享同一個MyQueue實例,因為集合中只會存儲對象的引用符,而所有對象的引用符都占用4 個字節。
除了定義泛型類,還可定義泛型方法、泛型接口、泛型結構體、泛型委托等。和泛型類一樣,使用泛型方法時也要為把抽象類型具體化。泛型的定義中可含有多個類型參數。例如泛型類class Dictionary<K,V>{…}。
泛型最常見的用途是定義泛型集合,泛型集合和相對應的非泛型集合功能基本相同,但泛型集合能提供嚴格的類型檢查,具有較強的安全性。此外,如果元素類型為值類型,泛型集合的性能通常優於非泛型集合。泛型集合類定義在System.Collections.Generic 命名空間中。
(2)列表
列表在非泛型集合中用 ArrayList 類實現,在泛型集合中用List<T>類實現。列表與數組類非常類似,其區別是數組不能改變大小,而列表可以改變大小。
List<string> basketballPlayers = new List<string>(); //默認容量為0
List<string> basketballPlayers = new List<string>(10); //容量為10
basketballPlayers.Capacity = 10;//改變列表的容量
當列表的容量改變時,系統會分配新的內存空間,創建一個全新的列表,然后把原列表的內容復制到新列表中,最后刪除原列表。向已經裝滿的列表添中加新元素時,列表會采用“翻倍”的辦法增加容量。現在basketballPlayers 是一個容量為10 的列表,當添加第11 項時,就會創建一個新的容量為20 的列表。容量翻倍貌似浪費內存空間,但這是一個使列表“合適增長”的高效方法。如果每次添加一個新元素就重新創建一次列表,其效率低下可想而知。訪問列表元素的方式和數組一樣,都是通過索引來訪問。例如:string name = basketballPlayers[2]; 。List<T>類的部分重要方法:
Add()方法,Add()方法用於向列表中添加元素,新元素位於列表的末尾,例如basketballPlayers.Add("姚明");。
Remove()方法,Remove()方法用於刪除元素,參數為欲刪除的對象。如果刪除成功,返回true;如果刪除不成功或欲刪除的對象不存在,返回false,例如basketballPlayers.Remove("鄧肯");。
RemoveAt()方法,使用Remove()方法時,系統需要在列表中進行搜索,以便找到匹配的元素,這個搜索過程需要花費一定的時間。RemoveAt()方法的參數為元素的索引,可以直接刪除指定位置上的元素。例如basketballPlayers.RemoveAt(2);。
Insert()方法,Insert()方法用於在指定位置插入元素。basketballPlayers.Insert(2, "諾維斯基");
RemoveRange()方法,RemoveRang()方法可以一次從列表中刪除多個元素,它的第一個參數表示起始位置,第二個參數表示從該位置起刪除幾個元素。例如basketballPlayers.RemoveRange(1, 3);。
AddRange()方法,AddRange()方法用於一次性添加一批元素,它的參數是一個集合,集合中包含所有要添加的元素。例如:string[] names ={ "鄧肯", "阿倫", "加索爾" }; basketballPlayers.AddRange(names);。
GetRange()方法,GetRange()方法用於獲取列表中指定范圍內的元素,它的第一個參數表示起始位置,第二個參數表示從該位置起讀取幾個元素。List<string> bestPlayers = basketballPlayers.GetRange(0,3);。
(3)棧
棧是一種后進先出(Last In First Out,LIFO)的集合類型,棧在非泛型集合中用Stack類實現,在泛型集合中用Stack<T>類實現,下面介紹一下泛型類Stack<T>中的重要屬性和方法。
Push()方法,在棧頂添加元素的操作稱為壓棧,用Push()方法實現,例如Stack<char> alphabet = new Stack<char>(); alphabet.Push('A');。
Pop()方法,從棧頂取出元素的方法稱為出棧,用 Pop()方法實現,例如char leter = alphabet.Pop();。使用Pop()方法后元素將從棧中刪除。
Peek()方法,用來讀取棧頂的元素,但不刪除它。
(4)隊列
隊列(Queue),隊列的元素只能從隊頭取出,從隊尾加入,是一種先進先出(First In First Out,FIFO)的集合類型。隊列在非泛型集合中用 Queue 類實現,在泛型集合中用Queue <T>類實現,下面介紹一下泛型類Queue <T>中的重要屬性和方法。
Enqueue()方法,在隊尾添加元素稱為入隊,用 Enqueue()方法實現。例如
Queue<char> alphabet = new Queue<char>();
alphabet.Enqueue('A');
alphabet.Enqueue('B');
遍歷時,也將按入隊的順序顯示。
Dequeue()方法,從隊頭取出元素稱為出隊,用Dequeue()方法實現,取出的元素會被刪除。
Peek()方法,Queue 類也有Peek()方法,它用來讀取隊頭的元素,但不刪除元素。
(5)排序列表
排序列表與列表很相似,區別是排序列表中的每個元素都與一個用於排序的鍵(Key)關聯,元素按鍵的順序排列。實際上排序列表內部維護兩個數組,一個數組用於存儲元素,另一個數組用於存儲與元素關聯的鍵。排序列表在非泛型集合中用 SortedList 類實現,在泛型集合中用SortedList<TKey,TValue>類實現,下面介紹一下泛型類SortedList<TKey, TValue>中的重要屬性和方法。
通過 SortedList<TKey, TValue>類的Add()方法可以向集合中添加元素,它有兩個參數,第一個參數是與元素關聯的鍵,第二個參數是元素的值。例如:
SortedList<int, string> medalTable = new SortedList<int, string>(); medalTable.Add(3, "吳敏霞");
medalTable.Add(1, "郭晶晶");
medalTable.Add(2, "哈莉娜");
//輸出結果
for (int i = 0; i < medalTable.Count; i++)
{Console.WriteLine(medalTable.Keys[i] + " : " + medalTable.Values[i]);}
SortedList<TKey, TValue>類的Count 屬性為列表中實際存儲的元素個數,Keys 屬性為所有鍵的集合,Values 屬性為所有值的集合。上述程序輸出結果為:1:郭晶晶 2:哈莉莉 3:吳敏霞。可以發現,排序列表中的元素並不是按添加順序排列的,而是按照鍵的順序排列的。
(6)散列表
散列表(Hashtable)又叫做字典(Dictionary),能夠非常快速的添加、刪除和查找元素,是現在檢索速度最快的數據結構之一。
散列函數能根據Key 直接計算出元素的索引,能否設計一個有效的散列函數,是解決問題的關鍵,然而現實世界並非總是那么完美,實際數據也並不總是那么配合散列函數,會出現各種各樣的問題。一種問題是元素的散列碼不連續,這種問題很好解決,大不了讓不連續的地方空在那里即可,雖然有點浪費空間,但因為散列表的讀寫速度很快,我們“以空間換取了時間”。另一種問題是多個Key具有相同的散列碼。。.NET采用了一種精心設計的方法解決沖突①,其基本思想是先通過散列函數計算出基位置,如果發現基位置已經被占用,就根據一定的算法向下尋找,直到找到空位置為止。當然,與之對應,檢索元素時也要采取相同的方法。。在實際問題中,散列函數有很多種,我們需要根據Key 的類型和整個集合的特征設計恰當的散列函數。一個設計良好的散列表,應當能使元素均勻分布。
這里講了散列表的基本原理,實際問題要復雜得多,但基本思想就是一條:用散列函數把元素映射到相應的位置。散列表中的元素可以是基本類型數據,也可以是非常復雜的包含很多成員變量的對象,這時我們需要挑選一個合適的成員變量做鍵(Key),而整個元素則被稱為值(Value)。,.NET 中所有的類都有一個從Object 類繼承來的GetHashCode()方法,散列表就是根據這個散列函數計算散列碼的。任何類型都可以用作Key,如果你用.NET中預定義的類做Key,散列表就通過該類的GetHashCode()方法計算散列碼;如果你想用自己定義的類做Key,一般需要重寫GetHashCode()方法。除此之外,散列表還允許我們根據需要指定散列函數,這時不管你的Key 為何種類型,都使用你所指定的散列函數進行計算。
散列表在非泛型集合類中用Hashtable 類實現,在泛型集合類中用Dictionary<TKey,TValue>類實現,下面我們介紹泛型類Dictionary<TKey, TValue>的重要屬性和方法。
①構造函數
Dictionary<TKey, TValue>類為我們重載了7 個構造函數,使用該泛型類時需要指明鍵類型和值類型。
Dictionary<string,Color> colorTable1 = new Dictionary<string,Color>();
除此之外,我們還可為字典提供自定義的GetHashCode()方法。
Dictionary<string,Color> colorTable1 = new Dictionary<string,Color>(comparer);
其中參數comparer 是一個實現了IEqualityComparer 接口的對象,IEqualityComparer 接口定義了兩個方法,Equals()方法用來判斷兩個對象是否相等,GetHashCode()方法用來為字典提供散列函數,這時不管字典中的Key 為何種類型,都將通過comparer 對象提供的散列函數進行散列。為了使字典能夠正常工作,編寫GetHashCode()方法有相當嚴格的要求。
②Add()方法
通過 Add()方法向字典中添加元素,它接受兩個參數,第一個參數是與元素關聯的鍵(Key),第二個參數是元素的值(Value),字典將根據Key 計算出元素的存儲位置。例如:
//創建元素
Color red = new Color("Red", 255, 0, 0);
Color green = new Color("Green", 0, 255, 0);
Color orange = new Color("Orange", 255, 200, 0);
//建立字典
Dictionary<string, Color> colorTable = new Dictionary<string, Color>();
//向字典中添加元素
colorTable.Add(red.Name, red);
colorTable.Add(green.Name, green);
colorTable.Add(orange.Name, orange);
先創建了一個字典,然后向字典中添加了三個Color 對象。這里選取Color 對象的Name 屬性作為Key,通過它計算元素在字典中的位置。
③以Key為索引設置或讀取元素
colorTable["Blue"] = new Color("Blue", 0, 0, 255); //設置元素:
Console.WriteLine("新添加的元素為:\n" + colorTable["Blue"]);//輸出元素
以這種方法向字典中添加元素與用 Add()方法有一點區別:當字典中已存在該Key 時,Add()方法會引發異常,而以Key 為索引則不會,只是用新值替換舊值而已。
④Keys屬性
通過字典的 Keys 屬性可以獲取字典的所有鍵。例如foreach (string key in colorTable.Keys){}
⑤Values屬性
通過字典的Values屬性可以獲取字典的所有元素的值,例如:
Console.WriteLine("字典中的值:");
foreach (Color value in colorTable.Values)
{Console.WriteLine(value);}
其結果如下圖所示。
⑥ContainsKey()方法,ContainsKey()方法用於檢驗字典中是否包含特定的鍵。因為參數提供了鍵的信息,所以可通過散列函數找到元素的位置,只需計算一次,效率很高。例如:if (colorTable.ContainsKey("Red")){…}
⑦ContainsValue()方法,用於檢驗字典中是否包含特定的Value。由於沒有鍵的信息,該方法需要檢索整個字典的來尋找元素,平均需要檢索n/2 次。例如if(colorTable.ContainsValue(black)){}
⑧Remove()方法,通過字典的 Remove()方法可以刪除與指定的鍵關聯的元素。例如:colorTable.Remove("Orange");
綜上所述,字典是一種檢索速度非常快的數據結構,在擁有海量數據的問題中往往能發揮非常重要的作用。
2.類型約束
在泛型中,類型參數T 可以實例化為任何數據類型,這在某種情況下會產生問題。
//T 代表某種動物類
class AnimalFamily<T>
{//用來存儲動物的家庭成員
private List<T> family = new List<T>();
//方法:添加成員
public void Add(T member)
{family.Add(member);}
//方法:顯示所有成員
public void Display()
{ foreach (T member in family)
{Console.WriteLine(member.name);}
}
}
主述程序會出現以下編譯錯誤,這是因為member 的類型為T,而T 為何種具體類型並不確定,因此就不能確定member 是否擁有成員變量name。
解決辦法是對抽象類型 T 進行約束(Constraint),限制T 的取值范圍。例如將代碼改成如下,在泛型類AnimalFamily<T>中,我們通過where 關鍵字把類型T 的范圍限制在Animal類和它的派生類中,這樣就確保對象member 中一定包含成員name,編譯也能順利通過了。
class Animal
{//公有變量
public string name;
//構造函數
public Animal(string nameValue)
{name = nameValue;} }
//泛型類
class AnimalFamily<T> where T : Animal
{…}