下表总结了最常用的一些集合类:
1.1List<T>集合类
泛型List<T>类是最简单的集合类。用法和数组差不多,可以使用标准数组语法(方括号和元素索引)来引用集合中的元素(但不能用这种语法在集合初始化之后添加新元素)。
List<T>类比数组灵活,避免了数组的以下限制。
(1)为了改变数组大小,必须创建新数组,复制数组元素(如果新数组较小,甚至还复
制不完),然后更新对原始数组的引用,使其引用新数组。
(2)删除一个数组元素,之后所有元素都必须上移-一位。即使这样还是不好使,因为最后一个元素会产生两个拷贝。
(3)插入一个数组元素,必须使元素下移一位来腾出空位。但最后一个元素就丢失了!
![](/qrcode.jpg)
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#从入门到精通》