首页 > 代码库 > 第十八章、使用集合

第十八章、使用集合

  什么是集合类

  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; }

第十八章、使用集合