18 使用集合

下表總結了最常用的一些集合類:

1.1List<T>集合類

泛型List<T>類是最簡單的集合類。用法和數組差不多,可以使用標準數組語法(方括號和元素索引)來引用集合中的元素(但不能用這種語法在集合初始化之後添加新元素)。

List<T>類比數組靈活,避免了數組的以下限制。

 

(1)爲了改變數組大小,必須創建新數組,複製數組元素(如果新數組較小,甚至還復

制不完),然後更新對原始數組的引用,使其引用新數組。

(2)刪除一個數組元素,之後所有元素都必須上移-一位。即使這樣還是不好使,因爲最後一個元素會產生兩個拷貝。

(3)插入一個數組元素,必須使元素下移一位來騰出空位。但最後一個元素就丟失了!

List<T>集合類通過以下功能來避免這些限制。

(4)創建List<T>集合時無需指定容量,它能隨元素的增加而自動伸縮。這種動態行爲當然是有開銷的,如有必要可指定初始大小。超過該大小,List<T>集合 自動增大。

(5)可用 Remove方法從List<T>集合刪除指定元素。List<T>集合自動重新排序並關閉裂口。還可用RemoveAt方法刪除List<T>集合指定位置的項。

(6)可用 Add方法在List<T>集合尾部添加元素。只需提供要添加的元素,List<T>集合的大小會自動改變。

(7)可用Insert方法在List<T>集合中部插入元素。同樣地,List<T>集合的大小會自動改變。

(8)可調用Sort方法輕鬆對List<T>對象中的數據排序。

 

注意:和數組一樣,用foreach遍歷List<T>集合時,不能用循環變量修改集合內容。另外,在遍歷List<T>的foreach循環中不能調用Remove, Add或Insert 方法,否則會拋出InvalidoperationException。

 

下例展示瞭如何創建,處理和遍歷一個List<int>集合的內容。

using System;
using System.Collections ,Generic;

..

List<int> numbers = new List<int>();
//使用Add方法填充List<int>
foreach (int number in new int[12]{10, 9,8, 7,7,6,5,10,4,3,2, 1))
{
    numbers.Add(number);
}

//在列表倒數第二個位置插入一個元素
//第一個參數是位置,第二個參數是要插入的值
numbers.Insert(numbers.Count-1, 99);
//刪除值是7的第一個元素(第4個元素,索引3)
numbers.Remove(7);
//刪除當前第7個元素,索引6 (10)
numtbers.RemoveAt(6);
//用for語句遍歷剩餘11個元素
Console.WriteLine("Iterating using a for statement:");
for (int i = 0; i < numbers.Count; 1++)
{
    int number = numbers[1]; //注意,這裏使用了數組語法
    Console.WriteLine(number);
}
//用foreach語句遍歷同樣的11個元素
Console .WriteLine("\nIterating using a foreach statement:");
foreach (int nunber in numbers)
{
    Console .WriteLine(number);
}

代碼的輸出如下所示:

Iterating using a for statement:

10

9

8

7

6

5

4

3

2

99

1

Iterating using a foreach statement:

10

9

8

7

6

5

4

3

99

1

注意:List<T>集合和數組用不同的方式判斷元素數量。列表是用Count屬性,數組是用Length屬性。

 

1.2LinkedList<T>集合類

LinkedList<T>集合類實現了雙向鏈表。列表中每- -項除了容納數據項的值,還容納對下一項的引用(Next屬性)以及對上一項的引用(Previous屬性)。列表起始項的Previous屬性設爲null,最後一項的Next屬性設爲null。

 

和List<T>類不同,LinkedList<T>不 支持用數組語法插入和檢查元素。相反,要用AddFirst方法在列表開頭插入元素,下移原來的第一項並將它 的Previous屬性設爲對新項的用。或者用AddLast方法在列表尾插入元素,將原來最後一項的Next屬性設爲對新項的引用。還可使用AddBefore和AddAfter方法在指定項前後插入元素(要先獲取項)。

 

First屬性返回對LinkedList<T>集合第一- 項的引用,Last 屬性返回對最後-項的引用。遍歷鏈表可從任何一端開始,查詢Next或Previous引用,直到返回null爲止。還可使用foreach語句正向遍歷LinkedList<T>對象,抵達末尾會自動停止。

 

從LinkedList<T>集合中刪除項是使用Remove, RemoveFirst 和Removelast方法。

 

下例展示了一個LinkedList<T>集合。注意如何用for語句遍歷列表,它查詢Next(或Previous)屬性,直到屬性返回null引用(表明已抵達列表末尾)。

using System;
using System.Collections . Generic;

...

LinkedList<int> numbers = new LinkedList<int>();
//使用AddFirst方法填充列表
foreach (int number in new int[] { 10,8,6,4,2 })
{
    numbers .AddFirst(number);
}
//用for語句遍歷
Console.WriteLine( "Iterating using a for statement:");
for (LinkedListNodecint> node = numbers.First; node l= null; node = node.Next)
{
    int number = node . Value;
    Console.WriteLine(number);
}

//用foreach語句遍歷
Console.Writeline("\nIterating using a foreach statement:");
foreach (int mumber in numbers)
{
    Console.WriteLine(number);
}


//反向遍歷(只能用for, foreach 只能正向遍歷)
Console.WriteLine("\nIterating list in reverse order:");
for (LinkedListNode<int> node = numbers.Last; node 1= null; node = node.Prevlous)
{
    int number = node.Value;
    Console.WriteLine(number);
}

代碼的輸出如下所示:

Iterating using a for statement:

2

4

6

8

10

Iterating using a foreach statement:

2

4

6

8

10

Iterating list in reverse order:

10

8

6

4

2

 

1.3Queue<T>集合類

 

Queue<T>類實現了先入先出隊列。元素在隊尾插入(入隊或Enqueue), 從隊頭移除(出

隊或Dequeue)。

下例展示了一個Queue<int>集合及其常見操作:

using System;
using System.Collections ,Generic;


Queuecint> numbers = new Queuecint>();
//填充隊列
Console.WriteLine("Populating the queue:");
foreach (int numiber in new int[4]{9, 3, 7, 2})
{
    numbers. Enqueue(number);
    Console.WriteLine($" (number} has joined the queue");
}

//遍歷隊列
Console.WriteLine("\nThe queue contains the followding items:");
foreach (int number in numbers)
{
    Console.WriteL ine(number);
)


//清空隊列
Console.Writel ine("\nDraining the queue:");
while (numbers .Count>0)
{
    int number = numbers .Dequeue();
    Console.Writel ine($"(nunber} has left the queue");
}

上述代碼的輸出如下:

Populating the queue:

9 has joined the queue

3 has joined the queue

7 has joined the queue

2 has joined the queue

The queue contains the following items:

9

3

7

2

Draining the queue:

9 has left the queue

3 has 1eft the queue

7 has left the queue

2 has 1eft the queue

 

1.4Stacke<T>集合類

Stack<T>類實現了後入先出的棧。元素在項部入棧(push),從頂部出棧(pop)。通常可以將棧想象成一疊盤子:新盤子疊加到項部,同樣從項部取走盤子。換言之,最後一個入棧的總是第一個被取走的。下面是一個例子(注意foreach循環列出項的順序):

using System;
using System.Collections.Generic;

…

Stack<int> numbers = new Stack<int>();
//填充棧-入棧
Console.WriteLine("Pushing itens onto the stack:");
foreach (int number in new int[4]{9, 3, 7, 2))
{
    members. Push(number);
    Console.WiriteLine($"(number} has been pushed on the stack");
)

//遍歷棧
Console .WriteLine("\nThe stack now contains:");
foreach (int number in nunmbers)
{
    console.WriteLine(number);
}


//清空棧
Console.WriteLine("\nPopping items from the stack:");
while (numbers.Count > 0)
{
    int number = numbers .Pop();
    Console.WriteLine($"{number} has been popped off the stack");
}

下面是程序的輸出:

Pushing items onto the stack:

9 has been pushed on the stack

3 has been pushed on the stack

7 has been pushed on the stack

2 has been pushed on the stack

The stack now contains :

2

7

3

9

Popping items from the stack:

2 has been popped off the stack

7 has been popped off the stack

3 has been popped off the stack

9 has been popped off the stack

 

1.5Dictionary<TKey, TValue>集合類

數組和List<T> 類型提供了將整數索引映射到元素的方式。在方括號中指定整數索引(例如[4])來獲取索引4的元素(實際是第5個元素)。但有時需要從非int類型(比如string,double或Time)映射。其他語言一般把這稱 爲關聯數組。C#的Dictionary<TKey, TValue>類在內部維護兩個數組來實現該功能。-一個存儲要從其映射的鍵,另一個存儲映射到的值。分別稱爲鍵數組和值數組。在Dictionary<TKey, TValue>集 閤中插入鍵/值對時,將自動記錄哪個鍵和哪個值關聯,允許開發人員快速、簡單地獲取具有指定鍵的值。Dictionary<TKey, TValue>類的設計產生了-些重要後果。

 

(1)Dictionary<TKey, TValue>集合 不能包含重複的鍵。調用Add方法添加鍵數組中已有的鍵將拋出異常。但是,如果使用方括號記號法來添加鍵/值對(參見後面的例子),就不用擔心異常一即 使之前已添加了相同的鍵。如果鍵已經存在,其值就會被新值覆蓋。可用ContainKey 方法測試Dictionary<TKey, TValue>集合是否已包含特定的鍵。

(2)Dictionary<TKey, TValue>集合內部採用一種稀疏數據結構,在有大量內存可用時才最高效。隨着更多元素的插入,Dictionary<TKey, TValue>集合可能快速消耗大量內存。

(3)使用foreach 語句遍歷Dictionary<TKey, TValue>集 合返回的是一個KeyValuePair<TKey, TValue>.。這是一個結構,包含的是數據項的鍵和值元素的拷貝,可通過Key和Value屬性訪問每個元素。元素是隻讀的,不能用它們修改Dictionary<TKey, TValue> 集合中的數據

下例將家庭成員年齡和姓名關聯並打印信息。
 

using System;
using System. collections .Generic;

…

Dictionary<string, int> ages  = new Dictionary<string, int>();
//填充字典
ages .Add("John", 51); //使用Add方法
ages .Add("Diana", 50);
agesI["anes"] = 23; /使用數組語法
ages["Francesca"] = 21;

//用foreach語句遍歷字典
//迭代器生成的是個KeyValuePair項
Console.WriteLine("The Dictionary contains:");
foreach (KeyWaluePair<string, int> element in ages)
{
    string name = element .Key;
    int age = element .Value;
    Console .WriteLine($"Name: {name}, Age: {age}");
}

程序輸出如下所示:

The Dictionary contains:

Name: John, Age: 51

Name: Diana, Age: 50

Name: James, Age: 23

Name: Francesca, Age: 21

 

1.6SortedList<Tkey,TValue>集合類

SortedList<TKey, TValue>類 與Dictionary<TKey, TValue>類很相似, 都允許將鍵和值關聯。主要區別是,前者的鍵數組總是排好序的(不然也不會叫SortedList 了)。在SortedList<TKey, TValue>對 象中插入數據花的時間比Dictionary<TKey,TValue>對象長,但獲取數據會快-一些(至少一樣快), 而且SortedList<TKey, TValue>類消耗內存較少。

在SortedList<Tkey, TValue>集合中插入- 一個鍵/值對時,鍵會插入鍵數組的正確索引位置,目的是確保鍵數組始終處於排好序的狀態。然後,值會插入值數組的相同索引位

置。SortedList<TKey, TValue>類自動保證鍵和值同步,即使是在添加和刪除了元素之後。

這意味着可按任意順序將鍵/值對插入-一個SortedList<TKey, TValue>, 它們總是按照鍵的值來排序。

 

與Dictionary<TKey, TValue>類相似, SortedList<TKey, TValue>集合不能包含重複鍵。用foreach語句遍歷SortedList<TKey, TValue>集合返回的是KeyValuePair<TKey,TValue>項,只是這些KeyValuePair<TKey, TValue>對 象已按Key屬性排好序。

 

下例仍然將家庭成員的年齡和姓名關聯並打印結果。但這一次使用的是有序列表而不是字典。

using System;
using System.Collections .Generic;
…


Sortedlist<string, int> ages = new SortedList<string, int>();

//填充有序列表
ages.Ad("John", 51); //使用Add方法
ages .Add("Diana", 50);
ages["James"] = 23;//使用數組語法
ages["Francesca"] = 21;

//用foreach語句遍歷有序列表
//迭代器生成的是一個KeyWaluePair項
Console.WriteLine("The SortedList contains:");
foreach (KeyaluePair<string, int> element in ages)
{
    string name = element .Key;
    int age = element.Value;
    Console.WriteLine($"Name: {name), Age: {age}");
}

結果按家庭成員姓名(鍵)的字母順序進行排序(D-F-J-J);

The SortedList contains:

Name: Diana, Age: 50

Name: Francesca, Age: 21

Name: James, Age: 2

Name: John, Age: 51

 

1.7HashSet<T>集合類

HashSet<T>類專爲集合操作優化,包括判斷數據項是否集合成員和生成並集/交集等。數據項用Add方法插入HashSet<T>集合,用Remove方法刪除。但是,HashSet<T>類真正強大的是它的IntersectWith, UnionWith 和Exceptwith 方法。這些方法修改HashSet<T>集合來生成與另一個HashSet<T>相交、合併或者不包含其數據項的新集合。這些操作是破壞性的,因爲會用新集合覆蓋原始HashSet<T>對象的內容。另外,還可以使用IsSubsetOf, IsSupersetof, IsProperSubsetof 和IsProperSupersetOf方法判斷一個HashSet<T>集合的數據是否另一個HashSet<T>集 合的超集或子集。這些方法返回Boolean值,是非破壞性的。

 

HashSet<T>集合內部作爲哈希表實現,可實現數據項的快速查找。但是,一個大的HashSet<T>集合可能需要消耗大量內存。

下例展示如何填充HashSet<T>集合並運用Intersectwith方法找出兩個集合都有的數據。

using System;
using System.Collections .Generic;

…

HastSet<string> employees = new Haset<string>(new string[] ("Fred',"Bert","Harry" ,"John"});
HashSet<string> castoners = new HshSet<string>(new string[] ({"John","Sid","Harry", "Diana"));
employees Add("James");
customers Add( "Francesca");
Console.WriteLine( "Employees:");
foreach (string nane in employees)
{
    Console ,WriteLine(name);
}
Console.Writel ine("\nCustomers:");
foreach (string name in customers)
{
    Console.WriteLine(nane);
}
Console.Writel Ine("\nCustomers who are also employees:"); //既是客戶又是員工的人
customers.。Intersectwith( ermployees);
foreach (string name in customers)
{
    Console .WriteLine(nane);
}

代碼的輸出如下所示:

Employees:

Fred

Bert

Harry

John

James

 

Customers :

John

sid

Harry

Diana

Francesca

 

Customers who are also employees:

John

Harry

 

注意:System.Collections .Generic命名空間還包含SortedSet<T>集合類型。工作方式和HashSet<T>相似。主要區別是數據保持有序。Sortedset<T> 和HashSet<T>類可以互操作。例如,可以獲取SortedSet<T>集合和HashSet<T>集合的並集。

 

2.使用集合初始化器

前面的例子展示瞭如何使用每種集合最合適的方法來添加元素。例如,List<T>使用Add, Queue<T>使用 Enqueue,而Stack<T>使用Push.一些集合類型還允許在聲明時使用和數組相似的語法來初始化。例如,以下語句創建並初始化名爲numbers的List<int>對象,這樣寫就不需要反覆調用Add方法了:

List<int> numbers =new List<int>(){10, 9, 8, 7, 7,6,5, 10, 4, 3, 2, 1});

C#編譯器內部會將初始化轉換成--系列Add方法調用。換言之,只有支持Add方法的集合才能這樣寫(Stack<T>和Queue<T>就不行)。

 

對於獲取鍵/值對的複雜集合(例如Dictionary<TKey, TValue>), 可用索引器語法爲每個鍵指定值,例如:

Dictionary<string, int> ages = new Dictionary<string, int>()
{
    ["John"] = 51,
    ["Diana"]=50,
    ["James"] = 23,
    ["Francesca"] = 21
};

如果願意,還可在集合初始化列表中將每個鍵/值對指定爲匿名類型,如下所示:

Dictionary<string, int> ages = new Dictlonary<string, int>()
{
    {"John", 51]},
    {"Dlana", 50},
    {"James", 23},
    {"Francesca", 21}
};

每一對的第一一項是鍵, 第二項是值。爲增強代碼可讀性,建議初始化字典類型時儘量使用索引器語法。

 

3.Find方法、謂詞和Lambda表達式

對於List<T>和LinkedList<T>等支持無鍵隨機訪問的集合,它們無法通過數組語法來查找項,所以專門提供了Find方法。Find 方法的實參是代表搜索條件的謂詞。謂詞就是一個方法,它檢查集合的每一項,返回Boolean值指出該項是否匹配。

Find方法返回的是發現的第一個匹配項。List<T>和LinkedList<T>類還支持其他方法,例如FindLast返回最後一個匹配項。List<T>類還專門有一個FindAll 方法,返回所有匹配項的一個List<T>集合。

 

謂詞最好用Lambda表達式指定。簡單地說,Lambda 表達式是能返回方法的表達式。

 

一般的方法由4部分組成:返回類型、方法名、參數列表和方法主體。

Lambda表達式只由兩部分組成:參數列表和方法主體。

 

在Find方法的情況下,謂詞依次處理集合中的每一項: 謂詞的主體必須檢查項,根據是否匹配搜索條件返回true或false.以下加粗的語句在一一個 List<Person>上調用Find方法(Person是結構),返回ID屬性爲3的第一項。

struct Person
{
    public int ID { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}

...
//創建並填充personnel列表
List<Person) personnel . new List<Person>()
{
    new Person() { ID= 1, Name="John", Age = 51 },
    new Person() { ID = 2, Name="Sid", Age =28 },
    new Person() { ID=3, Name="Fred", Age = 34 },
    new Person() { ID=4, Name="Paul", Age=22 },
};


//查找ID爲3的第一個列表成員
Person match = personnel.Find((Person  p) =>{ return p.ID== 3; };
Console.WriteLine($"ID: {match. ID}\nName: {match.Name}\nAge: {match.Age}");

上述代碼的輸出如下:

ID: 3

Name: Fred

Age: 34

 

調用Find方法時,實參(Person p) => { return p.ID== 3; }就是實際“幹活兒”的Lambda表達式,它包含以下語法元素。

(1)圓括號中的參數列表。和普通方法一樣,即使Lambda表達式代表的方法不獲取

任何參數,也要提供一對空白圓括號。 對於Find 方法,謂詞要針對集合中的每

一項運行, 該項作爲參數傳給Lambda表達式。

(2)=>操作符, 它向C#編譯器指出這是一一個Lambda表達式。

(3)Lambda 表達式主體(方法主體)。本例的主體很簡單,只有一個語句,返回Boolean

值來指出參數所指定的項是否符合搜索條件。然而,Lambda表達式完全可以包含多個語句,而且可以採用你覺得最易讀的方式來排版。只是要記住,和普通方法一樣,每個語句都要以分號結束。

 

Lambda表達式的一些特點:

(1)如Lambda 表達式要獲取參數,就在=>操作符左側的圓括號內指定。 可省略參數類型,C#編譯器能根據Lambda表達式的上下文進行推斷。如希望Lambda表達式永久(而不是局部)更改參數值,可用“傳引用”方式傳遞參數(使用ref關鍵字),但不推薦這樣做。

(2)Lambda表達式可返回值,但返回類型必須與對應的委託的類型匹配。

(3)Lambda 表達式主體可以是簡單表達式,也可以是C#代碼塊(代碼塊可包含多個語句,方法調用、變量定義等等)。

(4)Lambda 表達式方法中定義的變量會在方法結束時離開作用城(失效)。

(5)Lambda表達式可訪問和修改Lambda 表達式外部的所有變量,只要那些變量在Lambda表達式定義時,和Lambda表達式處在相同作用城中。一定要非常留意這個特點!

 

4.比較數組和集合

(1)數組實例具 有固定大小,不能增大或縮小。集合則可根據需要動態改變大小。

(2)數組可以多維, 集合則是線性。但集合中的項可以是集合自身,所以可用集合的

集合來模擬多維數組。

(3)數組中的項通過索引來存儲和獲取。並非所有集合都支持這種語法。例如,要用Add或Insert方法在List<T>集合中存儲項,用Find方法獲取項。

(4)許多集合 類都提供了ToArray方法,能創建數組並用集合中的項來填充。複製到數組的項不從集合中刪除。另外,這些集合還提供了直接從數組填充集合的構造器。

 

參考書籍:《Visual C#從入門到精通》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章