参考书:《 visual C# 从入门到精通》
第三部分 用C#定义可扩展类型
第18章 使用集合
文章目录
18.1 什么是集合
Microsoft .NET Framework提供了几个类,它们在System.Collections.Generic
命名空间中。
下面为常用的集合类:
集合 | 说明 |
---|---|
List<T> | 可像数组那样按索引访问列表,提供了一些搜索和排序方法 |
Queue<T> | 先入先出,提供方法将数据项添加到队列的一端,从另一端删除项,以及只检查不删除 |
Stack<T> | 先入后出,提供方法将数据项压入栈顶,从栈顶出栈,和只检查栈顶不删除 |
LinkedList<T> | 双向有序列表,为任何一端插入和删除优化了。既可作为队列又可作为栈,还支持像列表那样的随机访问 |
HashSet<T> | 无序值列表,为快速数据获取而优化了。提供了面向集合的方法来判断它容纳的像是不是另一个HashSet对象中的项的子集,以及计算不同HashSet对象的交际和并集 |
Dictionary<TKey,TValue> | 字典集合,根据键来获取值 |
SortedList<TKey,TValue> | 键值对的有序列表。键必须 实现IComparable接口 |
18.1.1 List<T>集合类
List<T>
集合类相比较数组的优势在于:
- 创建时无需指定大小,能随元素的增加而自动伸缩。也可以指定初始大小,超过该大小时集合将增大
- 可用
Remove
方法删除指定元素,RemoveAt
方法删除指定位置的项 Add
方法在集合尾部添加元素Insert
方法在集合中间插入元素Sort
方法可对对象进行排序
如下面的程序简单的应用了List<T>类
using System;
using System.Collections.Generic;
namespace C_18_1_1
{
class Program
{
static void dowork()
{
List<int> number = new List<int>();
foreach (int num in new int[12] {10,2,3,4,5,6,7,8,9,0,7,3 })
{
number.Add(num);
}
number.Insert(number.Count - 1, 99);
number.Remove(7);
number.RemoveAt(6);
Console.WriteLine("Iterating uisng a for statement:");
for(int i=0;i < number.Count; ++i)
{
int num = number[i];
Console.WriteLine(num);
}
Console.WriteLine("\nIterating using a foreach statement:");
foreach(int num in number)
{
Console.WriteLine(num);
}
}
static void Main(string[] args)
{
dowork();
}
}
}
结果如下
Iterating uisng a for statement:
10
2
3
4
5
6
9
0
7
99
3
Iterating using a foreach statement:
10
2
3
4
5
6
9
0
7
99
3
C:\Users\xhh\Source\Repos\C_18_1_1\C_18_1_1\bin\Debug\netcoreapp3.1\C_18_1_1.exe (进程 25388)已退出,代码为 0。
按任意键关闭此窗口. . .
18.1.2 LinkedList<T>集合类
与List<T>不同的是,它不支持用数组语法插入和检查元素,要用AddFirst
方法在列表开头插入元素,下移原来的第一项并将它的previous
属性设为对新项的引用。或用AddLast
方法在列表尾插入元素,将原来的最后一项的Next
属性设为对新项的引用。也可用AddBefore
和AddAfter
在指定项前后插入元素。
First
属性返回对集合的第一项的引用,Last
属性返回对最后一项的引用。遍历链表可以从一端开始,查询Next
或previous
引用,直到返回null
为止。也可用foreach
语句正向遍历对象。
删除项可用Remove
,RemoveFirst
,RemoveLast
。
如下:
using System;
using System.Collections.Generic;
using System.Linq;
namespace C_18_1_2
{
class Program
{
static void dowork()
{
LinkedList<int> numbers = new LinkedList<int>();
foreach (int num in new int[] { 10, 8, 6, 4, 2 })
{
numbers.AddFirst(num);
}
Console.WriteLine("Iterating using a for statement:");
for (LinkedListNode<int> node = numbers.First; node != null; node = node.Next)
{
int num = node.Value;
Console.WriteLine(num);
}
Console.WriteLine("\nIteratin using a foreach statement:");
foreach(int num in numbers)
{
Console.WriteLine(num);
}
Console.WriteLine("\nIterating list in reverse order:");
for (LinkedListNode<int> node = numbers.Last; node != null; node = node.Previous)
{
int num = node.Value;
Console.WriteLine(num);
}
}
static void Main(string[] args)
{
dowork() ;
}
}
}
运行结果如下:
Iterating using a for statement:
2
4
6
8
10
Iteratin using a foreach statement:
2
4
6
8
10
Iterating list in reverse order:
10
8
6
4
2
C:\Users\xhh\Source\Repos\C_18_1_2\C_18_1_2\bin\Debug\netcoreapp3.1\C_18_1_2.exe (进程 24064)已退出,代码为 0。
按任意键关闭此窗口. . .
18.1.3 Queue<T>集合类
如下:
using System;
using System.Collections.Generic;
using System.Linq;
namespace C_18_1_2
{
class Program
{
static void dowork()
{
Queue<int> number = new Queue<int>();
Console.WriteLine("Populating the queue:");
foreach(int num in new int[] { 9, 3, 7, 2 })
{
number.Enqueue(num);
Console.WriteLine($"{num} has joined the queue");
}
Console.WriteLine("\n The queue contains the following items:");
foreach(int num in number)
{
Console.WriteLine(num);
}
Console.WriteLine("\nDraining the queue:");
while (number.Count > 0)
{
int num = number.Dequeue();
Console.WriteLine("${num} has left the queue");
}
}
static void Main(string[] args)
{
dowork() ;
}
}
}
运行结果:
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:
${num} has left the queue
${num} has left the queue
${num} has left the queue
${num} has left the queue
C:\Users\xhh\Source\Repos\C_18_1_2\C_18_1_2\bin\Debug\netcoreapp3.1\C_18_1_2.exe (进程 11760)已退出,代码为 0。
按任意键关闭此窗口. . .
18.1.4 Stack<T>集合类
using System;
using System.Collections.Generic;
using System.Linq;
namespace C_18_1_2
{
class Program
{
static void dowork()
{
Stack<int> numbers = new Stack<int>();
Console.WriteLine("Push items onto the stack:");
foreach(int num in new int[4] { 9, 3, 7, 2 })
{
numbers.Push(num);
Console.WriteLine($"{num} has been pushed on the stack");
}
Console.WriteLine("\n The stack new contains");
foreach(int num in numbers)
{
Console.WriteLine(num);
}
Console.WriteLine("\nPopping items from the stack:");
while (numbers.Count > 0)
{
int num = numbers.Pop();
Console.WriteLine($"{num} has been popped off the stack");
}
}
static void Main(string[] args)
{
dowork() ;
}
}
}
运行结果:
Push 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 new 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
C:\Users\xhh\Source\Repos\C_18_1_2\C_18_1_2\bin\Debug\netcoreapp3.1\C_18_1_2.exe (进程 28912)已退出,代码为 0。
按任意键关闭此窗口. . .
18.1.5 Dictionary<TKey,TValue>集合类
注意:
- 集合中不能包含重复的键。用
Add
方法添加键数组中已有的键将抛出异常。但如果用方括号记法添加键值对就不会有异常,添加相同的键会覆盖。可用ContainKey
方法来确定集合中是否含有特定的键 - 集合内部采用一中稀疏数据结构,有大量内存时是最高效的,插入元素会快速消耗大量内存
- 用
foreach
语句遍历集合返回的是一个KeyValuePair<TKey,TValue>
,其中包含数据项键和值元素的拷贝,可以通过Key
和Value
属性访问每个元素。元素是只读的,不能修改集合中的元素
using System;
using System.Collections.Generic;
using System.Linq;
namespace C_18_1_2
{
class Program
{
static void dowork()
{
Dictionary<string, int> ages = new Dictionary<string, int>();
ages.Add("John", 51);
ages.Add("Diana", 50);
ages["James"] = 23;
ages["Francesca"]=21;
Console.WriteLine("The Dictionary contains:");
foreach(KeyValuePair<string,int>ele in ages)
{
string name = ele.Key;
int age = ele.Value;
Console.WriteLine($"Name: {name}, Age: {age}");
}
}
static void Main(string[] args)
{
dowork() ;
}
}
}
运行结果:
The Dictionary contains:
Name: John, Age: 51
Name: Diana, Age: 50
Name: James, Age: 23
Name: Francesca, Age: 21
C:\Users\xhh\Source\Repos\C_18_1_2\C_18_1_2\bin\Debug\netcoreapp3.1\C_18_1_2.exe (进程 30456)已退出,代码为 0。
按任意键关闭此窗口. . .
18.1.6 SortedList<TKey,TValue>集合类
SortedList
的键总是排好序的,在对象中插入数据花的时间比Dictionary
对象长,但获取数据会快一些,而且它消耗的内存较少。
using System;
using System.Collections.Generic;
using System.Linq;
namespace C_18_1_2
{
class Program
{
static void dowork()
{
SortedDictionary<string, int> ages = new SortedDictionary<string, int>();
ages.Add("John", 51);
ages.Add("Diana", 50);
ages["James"] = 23;
ages["Francesca"]=21;
Console.WriteLine("The SortedList contains:");
foreach(KeyValuePair<string,int>ele in ages)
{
string name = ele.Key;
int age = ele.Value;
Console.WriteLine($"Name: {name}, Age: {age}");
}
}
static void Main(string[] args)
{
dowork() ;
}
}
}
运行结果:
The SortedList contains:
Name: Diana, Age: 50
Name: Francesca, Age: 21
Name: James, Age: 23
Name: John, Age: 51
C:\Users\xhh\Source\Repos\C_18_1_2\C_18_1_2\bin\Debug\netcoreapp3.1\C_18_1_2.exe (进程 13132)已退出,代码为 0。
按任意键关闭此窗口. . .
18.1.7 HashSet<T>集合类
HashSet<T>
类专为集合操作优化。Add
和Remove
分别是插入和删除操作。HashSet有比较强大的方法:IntersectWith
、UnionWith
和ExceptWith
,分别是生成与另一个集合相交、合并或者不包含其数据项的新集合,这些操作会覆盖原始HashSet<T>
对象的内容。还有IsSunsetOf
、IsSupersetOf
、IsProperSubsetOf
和IsProperSupersetOf
判断集合的数据是否是另一个集合的超集或子集。
集合内部作为哈希表的实现,可以实现数据项的快速查找。但一个大的集合需要消耗大量的内存。
using System;
using System.Collections.Generic;
using System.Linq;
namespace C_18_1_2
{
class Program
{
static void dowork()
{
HashSet<string>employees=new HashSet<string>( new string[]{ "Fred", "Bert", "Harry", "John" });
HashSet<string> customers = new HashSet<string>(new string[] { "John", "Sid", "Harry", "Diana" });
employees.Add("James");
customers.Add("Francesca");
Console.WriteLine("Employees:");
foreach (string name in employees)
Console.WriteLine(name);
Console.WriteLine("\nCustomers:");
foreach (string name in customers)
Console.WriteLine(name);
Console.WriteLine("\nCustomers who are also employees:");
customers.IntersectWith(employees);
foreach (string name in customers)
Console.WriteLine(name);
}
static void Main(string[] args)
{
dowork() ;
}
}
}
运行结果:
Employees:
Fred
Bert
Harry
John
James
Customers:
John
Sid
Harry
Diana
Francesca
Customers who are also employees:
John
Harry
C:\Users\xhh\Source\Repos\C_18_1_2\C_18_1_2\bin\Debug\netcoreapp3.1\C_18_1_2.exe (进程 20336)已退出,代码为 0。
按任意键关闭此窗口. . .
18.2 使用集合初始化器
一些集合允许在声明时用和数组类似的语法初始化:
List<int> numbers=new List<int>(){10,9,8};
编译器内部会将初始化转换成一系列的Add
方法调用。所以只有支持Add
方法的集合才可以这样写。
对于获取键值对的集合可以用索引器语法:
Dictionary<string,int> ages=new Dictionary<string int>(){
["John"]=51,
["Diana"]=50,
["James"]=23,
['Francesca']=21
}
也可以在集合初始化列表中将每个键值对指定为匿名类型:
Dictionary<string,int> ages=new Dictionary<string int>(){
{"John",51},
{"Diana",50},
{"James",23},
{'Francesca',21}
}
18.3 Find方法、谓词和Lambda表达式
对于支持随机访问的集合,它们无法通过数组语法来查找项,所以提供了Find
方法。Find
方法的实参是代表搜索条件的谓词。谓词是一个方法,它检查每一项,返回布尔值指出该项是否匹配。Find
方法返回发现的第一个匹配项。另外FindLast
返回最后一个匹配项,FindAll
返回所有匹配项的一个List<T>
集合。
谓词最好用Lambda
表达式指定。Lambda
表达式是能返回方法的表达式。
如:
struct Person{
public int ID{get;set;}
public string Name{get;set;}
public int Age{get;set;}
}
...;
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}
}
Person match=personnel.Find((Person p)=>{return p.ID==3});
其中的Lambda
表达式(Person p)=>{return p.ID==3}
,包含以下语法元素:
- 圆括号中的是参数列表
=>
操作符,向编译器指出这是一个Lambda
表达式- 表达式主体,可以包含多个语法
如果Lambda
表达式主体只有一个表达式,大括号和分号就可以省略,如果表达式只有一个参数,圆括号也可以省略,很多时候也可以省略参数类型,编译器会根据上下文推断。如:
Person match=personnel.Find(p=>p.ID==3);
Lambda
表达式具有以下特点:
- 在
=>
左侧的圆括号内指定要获取的参数,可省略参数类型。如希望全局更改参数值可以用关键字ref
- 可返回值,返回类型应与对应的委托类型匹配
- 表达式主体可以是简单表达式,也可以是代码块
- 表达式中定义的变量会在方法结束时离开作用域
- 表达式可以访问和修改表达式外部的所有变量,只要这些变量和
Lambda
表达式处在相同的作用域中
Lambda
表达式与匿名方法非常相似,但Lamdba
表达式要更灵活点,所以通常都是用Lambda
表达式。
18.4 比较数组和集合
我们再反过头来看一下第十章的卡牌游戏,只需要修改其中的两个源文件,这样用上我们这章学到的一些集合:
Pack.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace C_10_1_10
{
class Pack
{
public const int NumSuits = 4;
public const int CardsPerSuit = 13;
private Dictionary<Suit,List<PlayingCard>> cardPack;
private Random randomCardSelector = new Random();
public Pack()
{
this.cardPack = new Dictionary<Suit, List<PlayingCard>>(NumSuits);
for (Suit suit = Suit.Clus; suit <= Suit.Spades; ++suit)
{
List < PlayingCard > cardsInSuit = new List<PlayingCard>(CardsPerSuit);
for(Value value = Value.Two; value <= Value.Ace; ++value)
{
cardsInSuit.Add(new PlayingCard(suit,value));
}
this.cardPack.Add(suit, cardsInSuit);
}
}
public PlayingCard DealCardFrompack()
{
Suit suit = (Suit)randomCardSelector.Next(NumSuits);
while (this.IsSuitEmpty(suit))
{
suit = (Suit)randomCardSelector.Next(NumSuits);
}
Value value = (Value)randomCardSelector.Next(CardsPerSuit);
while (this.IsCardAlreadyDealt(suit, value))
{
value = (Value)randomCardSelector.Next(CardsPerSuit);
}
List<PlayingCard> cardsInSuit = this.cardPack[suit];
PlayingCard card = cardsInSuit.Find(c => c.CardValue() == value);
cardsInSuit.Remove(card);
return card;
}
private bool IsCardAlreadyDealt(Suit suit, Value value) {
List<PlayingCard> cardsInSuit = this.cardPack[suit];
return !cardsInSuit.Exists(c => c.CardSuit() == suit && c.CardValue() == value);
}
private bool IsSuitEmpty(Suit suit)
{
bool result = true;
for(Value value = Value.Two; value <= Value.Ace; value++)
{
if (!IsCardAlreadyDealt(suit, value))
{
result = false;
break;
}
}
return result;
}
}
}
Hand.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace C_10_1_10
{
class Hand
{
public const int HandSize = 13;
private List<PlayingCard> cards = new List<PlayingCard>(HandSize);
public override string ToString()
{
string result = "";
foreach(PlayingCard card in this.cards)
{
if(card!=null)
result += $"{card.ToString()}\n ";// card.ToString()+"/r/n";
}
return result;
}
public void AddCardToHand(PlayingCard cardDealt)
{
if (this.cards.Count >= HandSize)
{
throw new ArgumentException("Two many cards");
}
this.cards.Add(cardDealt);
}
}
}
其它的都保持不变就行,最终运行效果一样。