C# 篇基礎知識11——泛型和集合


.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

{…}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM