18 使用集合

下表总结了最常用的一些集合类:

1.1List<T>集合类

泛型List<T>类是最简单的集合类。用法和数组差不多,可以使用标准数组语法(方括号和元素索引)来引用集合中的元素(但不能用这种语法在集合初始化之后添加新元素)。

List<T>类比数组灵活,避免了数组的以下限制。

 

(1)为了改变数组大小,必须创建新数组,复制数组元素(如果新数组较小,甚至还复

制不完),然后更新对原始数组的引用,使其引用新数组。

(2)删除一个数组元素,之后所有元素都必须上移-一位。即使这样还是不好使,因为最后一个元素会产生两个拷贝。

(3)插入一个数组元素,必须使元素下移一位来腾出空位。但最后一个元素就丢失了!

扫描二维码关注公众号,回复: 10058804 查看本文章

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#从入门到精通》

发布了46 篇原创文章 · 获赞 53 · 访问量 3704

猜你喜欢

转载自blog.csdn.net/qq_38992372/article/details/105027520
18