第十八章、使用集合
什么是集合类
Microsoft .NET Framework提供了几个类,它们集合元素,并允许应用程序以特殊方式访问这些元素。这些类正是集合类,它们在System.Collections.Generic命名空间中。
List集合类
泛型List类是最简单的集合类。用法和数组差不多,可以使用标准数组语法(方括号和元素索引)来引用集合中的元素(但不能用这种语法在集合初始化之后添加新元素)。List类比数组灵活,避免了数组以下限制。
1、为了改变数组大小,必须创建新数组,复制数组元素(如果新数组较小,甚至还复制不完)。然后更新对原始数组的引用,使其引用新数组。
2、如果删除一个数组元素,之后的所有元素都必须上移一位。即使这样还不行,因为最后一个元素会产生两个拷贝。
3、如果插入一个数组元素,必须使元素下移一位来腾出空位。但最后一个元素就丢失了!
List集合类通过以下功能来避免这些限制:
1、不需要在创建List集合时指定容量,它能随着元素的增加而自动伸缩。这种动态行为当然是有开销的,如有必要可以指定初始大小。超过这个大小,List集合会自动增大。
2、可用Remove方法从List集合中删除指定元素。List集合自动重新排序并关闭裂口。还可以用RemoveAt方法删除List集合指定位置的项。
3、可用Add方法在List集合尾部添加元素。只需提供添加的元素,List集合的大小会自动改变。
4、可用Insert方法在List集合中部插入元素。同样地,List集合的大小会自动改变。
5、可调用Sort方法轻松对List对象中的数据排序。
List numbers = new List();
//使用Add方法填充List
foreach(int number in new int[12]{10,9,8,7,7,6,5,10,4,3,2,1})
{
numbers.Add(number); //10,9,8,7,7,6,5,10,4,3,2,1
}
//在列表倒数第二个位置插入一个元素
//第一个参数是位置,第二个参数是要插入的值
numbers.Insert(numbers.Count-1,99); //10,9,8,7,7,6,5,10,4,3,2,99,1
//删除值是7的第一个元素(第四个元素,索引4)
numbers.Remove(7);//10,9,8,7,6,5,10,4,3,2,99,1
//删除当前第7个元素,索引6(10)
numbers.Remove(7);//10,9,8,7,6,5,4,3,2,99,1
//用for语句遍历剩余11个元素
for(int i=0;i
{
int number = numbers[i];
Console.WriteLine(number); //10,9,8,7,6,5,4,3,2,99,1
}
//用foreach语句遍历剩余11个元素
foreach(int number in numbers)
{
Console.WriteLine(number);
}
LinkedList集合类
LinkedList集合类实现了双向链表。列表中的每一项除了容纳数据项的值,还容纳了对下一项的引用(Next属性)以及对上一项的引用(Previous属性)。列表起始项的Previous属性设为null,最后一项的Next属性设为null。
和List类不同,LinkedList不支持用数组语法插入和获取元素。相反,要用AddFirst方法在列表开头插入元素,下移原来的第一项并将它的Previous属性设为对新项的引用。或者用AddLast方法在列表尾插入元素,将原来最后一项的Next属性设为对新项的引用。还可使用AddBefore和AddAfter方法在指定项前后插入元素(要先获取项)。
First属性返回对LinkedList集合第一项的引用,Last属性返回对最后一项的引用。为了遍历链表,可以从它的任何一端开始,查询Next或Previous引用,直到null为止。还可以使用foreach语句正向遍历LinkedList对象,抵达末尾自动停止。
从LinkedList集合中删除项是使用Remove,RemoveFirst和RemoveLast方法。
LinkedList numbers = new LinkedList();
//使用AddFirst方法填充列表
foreach(int number in new int[]{10,8,6,4,2})
{
numbers.AddFirst(number); //2 , 4 , 6 , 8 ,`10
}
//用for语句遍历
Console.WriteLine("Iterating using a for statement:");
for(LinkedList node = numbers.First; node != null;node = node.Next)
{
int number = node.Value;
Console.WriteLine(number);//2 , 4 , 6 , 8 ,`10
}
//用foreach语句遍历
foreach(int number in numbers)
{
numbers.AddFirst(number); //2 , 4 , 6 , 8 ,`10
}
//反向遍历(只能用for,foreach只能正向遍历)
for(LinkedList node = numbers.Last; node != null;node = node.Previous)
{
int number = node.Value;
Console.WriteLine(number);//10,8,6,4,2
}
Queue集合类
Queue类实现了先入先出队列。元素在队尾插入(入队或Enqueue),从队头移除(出队或Dequeue)。
Queue numbers = new Queue();
//填充队列
Console.WriteLine("Populating the queue");
foreach(int number in new int[4]{9,3,7,2})
{
numbers.Enqueue(number);
Console.WriteLine("{0} has joined the queue",number);
}
//遍历队列
foreach(int number in numbers)
{
Console.WriteLine(number);
}
//清空队列
while(numbers.Count>0)
{
int number = numbers.Dequeue();
Console.WriteLine("{0} has left the queue",number);
}
Stack集合类
Stack类实现了后入先出的栈。元素在顶部入栈(push),从顶部出栈(pop)。通常可以将栈想象成一叠盘中:新盘子叠加到顶部,同样从顶部取走盘子。
Stack numbers = new Stack();
//填充栈—入栈
Console.WriteLine("Pushing items onto the stack:");
foreach(int number in new int[4]{9,3,7,2})
{
numbers.Push(number);
Console.WriteLine("{0} has been pushued on the stack",number);
}
//遍历栈
foreach(int number in numbers)
{
Console.WriteLine(number);//2,7,3,9
}
//清空栈
while(numbers.Count>0)
{
int number = numbers.Pop();
Console.WriteLine("{0} has been popped on the stack",number);}//2,7,3,9
}
Dictionary<tkey,tvalue>集合类
数组和List类型提供了将整数索引映射到元素的方式。在方括号中指定整数索引(例如[4])来获取索引4的元素(实际是第五个元素)。但有时需要从非int类型(比如string,double或Time)映射。其他语言一般把这称为关联数组。C#的Dictionary<tkey,tvalue>类在内部维护两个数组来实现该功能。一个keys数组容纳要从其映射的键,另一个value容纳映射到的值。在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>集合中的数据。
Dictionary<string,int> ages = new Dictionary<string,int>();
//填充字典
ages.Add("John",47); //使用Add方法
ages.Add("Diana",46);
ages["James"] = 20; //使用数组语法 可包含重复的键,如果键已存在,其值会被新值覆盖
ages["Francesca"] = 18;
//用foreach语句遍历字典
//迭代器生成的是一个KeyValuePair项
Console.WriteLine("The Dictionary contains:");
foreach(KeyValuePair<string,int> element in ages)
{
string name = element .Key;
int age = element .Value;
Console.WriteLine("Name:{0},Age:{1}",name,age);
}
SortedList<tkey,tvalue>集合类
SortedList<tkey,tvalue>类与Dictionary<tkey,tvalue>类非常相似,都允许将建和值关联。主要区别是,前者的keys数组总是排好序的。在SortedList<tkey,tvalue>对象中插入数据花的时间较长,但获取数据会快一些,而且SortedList<tkey,tvalue>类消耗的内存较少。
在SortedList<tkey,tvalue>集合中插入一个键/值对时,键会插入keys数组的正确索引位置,目的是确保keys数组始终处于排好序的状态。然后,值会插入values数组的相同索引位置。SortedList<tkey,tvalue>类自动保证键值同步,即使是在添加和删除了元素之后。这意味着可按任意顺序将键/值对插入一个SortedList<tkey,tvalue>,它们总是根据键来排序。
和Dictionary<tkey,tvalue>类相似,SortedList<tkey,tvalue>集合不能包含重复的键。用foreach语句遍历SortedList<tkey,tvalue>集合返回的是KeyValuePair<tkey,tvalue>对象,只是这些KeyValuePair<tkey,tvalue>对象会根据Key属性排好序。
SortedList<string,int> ages = new SortedList<string,int> ();
//填充有序列表
ages.Add("John",47); //使用Add方法
ages.Add("Diana",46);
ages["James"] = 20; //使用数组语法
ages["Francesca"] = 18;
//用foreach语句遍历有序列表
//迭代器生成的是一个KeyValuePair项
Console.WriteLine("The SortedList contains:");
foreach(KeyValuePair<string,int> element in ages)
{
string name = element .Key;
int age = element .Value;
Console.WriteLine("Name:{0},Age:{1}",name,age);
}
HashSet集合类
HashSet类专为集合操作优化,操作包括设置成员和生成并集/交集等。
数据项用Add方法插入HashSet集合,用Remove方法删除。但是,HashSet类真正强大的是它的IntersectWith,UnionWith,ExceptWith方法。这些方法修改HashSet集合来生成与另一个HashSet相交、合并或者不包含其数据项的新集合。这些操作是破坏性的,因为会用新集合覆盖原始HashSet对象的内容。另外,还可以使用IsSubsetOf,IsSupersetOf,IsProperSubsetOf,IsProperSupersetOf方法判断一个HashSet集合的数据是否另一个HashSet集合的超集或子集。这些方法返回Boolean值,是非破坏性的。
HashSet employees = new HashSet(new string[]{"Fred","Bert","Harry","John"});
HashSet customers = new HashSet(new string[]{"John","Sid","Harry","Diana"});
Concole.WriteLine("Employees:");
foreach(string name in employees)
{
Concole.WriteLine(name); //Fred Bert Harry John
}
Concole.WriteLine("\nCustomers:");
foreach(string name in customers )
{
Concole.WriteLine(name); //John Sid Harry Diana
}
Concole.WriteLine("\nCustomers who are also employees:");//既是客户又是员工的人
customers.IntersectWith(employees );//IntersectWith操作是破坏性的,新集合覆盖原来的customers
foreach(string name in customers )
{
Concole.WriteLine(name); //John Harry
}
使用集合初始化器
List numbers = new List(){10,9,8,7,7,6,5,10,4,3,2,1};
C#编译器内部会将初始化转换成一系列Add方法调用。换言之,只有支持Add方法的集合才能这样写。
对于获取键/值对的复杂集合,可在集合初始化器中将每个键/值对指定为匿名类型,如下:
Dictionary<string,int> ages = new Dictionary<string,int>(){{"John",47},{"Diana",46},{"James",21},{"Francesca",18}};
Find方法、谓词和Lambda表达式
面向字典的集合(Dictionary<tkey,tvalue>,SortedDictionary<tkey,tvalue>,SortedList<tkey,tvalue>)允许根据键来快速查找值,支持用数组语法访问值。对于List和LinkedList等支持无键随机访问的集合,它们无法通过数组语法来查找项,所以专门提供了Find方法。Find方法的实参是代表搜索条件的谓词。谓词就是一个方法,它检查集合的每一项,返回Boolean值指出该项是否匹配。Find方法返回的是发现的第一个匹配项。List和LinkedList类还支持其他方法,例如FindLast返回最后一个匹配项。List类还专门有一个FindAll方法,它返回所有匹配项的一个List集合。
谓词最好用Lambda表达式指定。简单地理解,Lanmda表达式是能返回方法的表达式。
方法通常是4部分组成:返回类型、方法名、参数列表和方法主体。但Lambda表达式只包含其中的两个元素:参数列表和方法主体。Lambda表达式没有定义方法名,返回类型(如果有的话)则根据Lambda表达式的使用上下文推断。
struct Person
{
public int ID{get;set;}
public string Name{get;set;}
public int Age{get;set;}
}
//创建并填充personnel列表
List personnel = new List<>(Person)
{
new Person(){ID =1, Name = "John", Age = 47},
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("{0},{1},{2}",match.ID,match.Name,match.Age);
调用Find方法时,实参(Person p)=>{return p.ID == 3; }就是实际“干活儿”的Lambda表达式,它包含以下语法元素。
1、圆括号中的参数列表。和普通方法一样,即使Lambda表达式代表的方法不获取任何参数,也要提供一对空白圆括号。如:(Person p)
2、=>操作符,它向C#编译器指出这是一个Lambda表达式。
3、Lambda表达式主体(方法主体)如:{return p.ID == 3; }