c#集合

合集下载
相关主题
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

集 合
第5章介绍了数组和Array 类执行的接口。

数组的大小是固定的。

如果元素个数是动态的,就应使用集合类。

List<T>和ArrayList 是与数组相当的集合类。

还有其他类型的集合:队列、栈、链表和字典。

本章介绍如何使用对象组。

主要内容如下:
● 集合接口和类型
● 列表
● 队列
● 栈
● 链表
● 有序表
● 字典
● 带多个键的字典
● 位数组
● 性能
10.1 集合接口和类型
集合类可以组合为集合,存储Object 类型的元素和泛型集合类。

在.NET 2.0之前,不存在泛型。

现在泛型集合类通常是集合的首选类型。

泛型集合类是类型安全的,如果使用值类型,是不需要装箱操作的。

如果要在集合中添加不同类型的对象,且这些对象不是相互派生的,例如在集合中添加int 和string 对象,就只需基于对象的集合类。

另一组集合类是专用于特定类型的集合,例如StringCollection 类专用于string 类型。

提示:
泛型的内容可参见第9章。

第 章 10
第Ⅰ部分 C# 语言
248 对象类型的集合位于System.Collections命名空间;泛型集合类位于System.Collections.
Generic命名空间;专用于特定类型的集合类位于System.Collections.Specialized命名空间。

当然,组合集合类还有其他方式。

集合可以根据集合类执行的接口组合为列表、集合和字典。

接口及其功能如表10-1所示。

.NET 2.0为集合类添加了新的泛型接口,例如IEnumerable<T>和IList<T>。

这些接口的非泛型版本将一个对象定义为方法的参数,而其泛型版本使用泛型类型T。

提示:
接口IEnumerable、ICollection和IList的内容详见第5章。

对集合非常重要的接口及其方法和属性如表10-1所示。

第10章集合
10.2 列表
.NET Framework为动态列表提供了类ArrayList和List<T>。

System.Collections.Generic
命名空间中的类List<T>的用法非常类似于System.Collections命名空间中的ArrayList类。

这个类实现了IList、ICollection和IEnumerable接口。

第9章讨论了这些接口的方法,所
以本节只探讨如何使用List<T>类。

下面的例子将Racer类中的成员用作要添加到集合中的成员,以表示一级方程式的一
位赛手。

这个类有三个字段:firstname、lastname和获胜者的数量。

这些字段可以用属性
来访问。

在该类的构造函数中,可以传送赛手的姓名和获胜者的数量,以设置成员。

方法ToString()重写为返回赛手的姓名。

类Racer也实现了泛型接口IComparer<T>,为Racer元
素排序。

[Serializable]
public class Racer : IComparable<Racer>, IFormattable
249
第Ⅰ部分 C# 语言
250 {
public Racer()
: this(String.Empty, String.Empty, String.Empty) {}
public Racer(string firstname, string lastname, string country)
: this(firstname, lastname, country, 0) {}
public Racer(string firstname, string lastname, string country, int wins) {
this.firstname = firstname;
stname = lastname;
this.country = country;
this.wins = wins;
}
private string firstname;
public string Firstname
{
get {return firstname; }
set { firstname = value; }
}
private string lastname;
public string Lastname
{
get {return lastname; }
set { lastname = value; }
}
private string country;
public string Country
{
get {return country; }
set { country = value; }
}
private int wins;
public int Wins
{
get {return wins; }
set { wins = value; }
}
public override string ToString()
{
return firstname + " " + lastname;
}
public string ToString(string format, IFormatProvider formatProvider) {
switch (format)
第10章集合
{
case null:
case "N": //Name
return ToString();
case "F": //FirstName
return Firstname;
case "L": //LastName
return Lastname;
case "W": //Wins
return ToString() + " Wins: " + wins;
case "C": //Country
return ToString() + " Country: " + country;
case "A": //All
return ToString() + ", " + country + " Wins: " + wins;
default:
throw new FormatException(String.Format(formatProvider,
"Format {0} is not supported", format));
}
}
public string ToString(string format)
{
return ToString(format, null);
}
public int CompareTo(Racer other)
{
return stname. CompareTo(stname);
}
}
10.2.1 创建列表
调用默认的构造函数,就可以创建列表对象。

在泛型类List<T>中,必须在声明中为
列表的值指定类型。

下面的代码说明了如何声明一个包含int的List<T>和一个包含Racer
元素的列表。

ArrayList是一个非泛型列表,可以将任意Object类型作为其元素接受。

使用默认的构造函数创建一个空列表。

元素添加到列表中后,列表的容量就会扩大为可
接纳4个元素。

如果添加了第5个元素,列表的大小就重新设置为包含8个元素。

如果8个
元素还不够,列表的大小就重新设置为16。

每次都会将列表的容量重新设置为原来的2倍。

ArrayList objectList = new ArrayList();
List<int> intList = new List<int>();
List<Racer> racers = new List<Racer>();
如果列表的容量改变了,整个集合就要重新分配到一个新的内存块中。

在List<T>的
实现代码中,使用了一个T类型的数组。

通过重新分配内存,创建一个新数组,Array.Copy()
将旧数组中的元素复制到新数组中。

为节省时间,如果事先知道列表中元素的个数,就可
251
第Ⅰ部分 C# 语言
以用构造函数定义其容量。

下面创建了一个容量为10个元素的集合。

如果该容量不足以容纳要添加的元素,就把集合的大小重新设置为20,或40,每次都是原来的2倍。

ArrayList objectList = new ArrayList(10);
List<int> intList = new List<int>(10);
使用Capacity属性可以获取和设置集合的容量。

objectList.Capacity = 20;
intList.Capacity = 20;
容量与集合中元素的个数不同。

集合中元素的个数可以用Count属性读取。

当然,容量总是大于或等于元素个数。

只要不把元素添加到列表中,元素个数就是0。

Console.WriteLine(intList.Count);
如果已经将元素添加到列表中,且不希望添加更多的元素,就可以调用TrimExcess()方法,去除不需要的容量。

但是,重新定位是需要时间的,所以如果元素个数超过了容量的90%,TrimExcess()方法将什么也不做。

intList.TrimExcess();
提示:
对于新的应用程序,通常可以使用泛型类List<T>替代非泛型类ArrayList,而且ArrayList类的方法与List<T>非常相似,所以本节将只介绍List<T>。

1. 添加元素
使用Add()方法可以给列表添加元素,如下所示。

实例化的泛型类型用Add()方法定义了第一个参数的类型:
List<int> intList = new List<int>();
intList.Add(1);
intList.Add(2);
List<string> stringList = new List<string>();
stringList.Add("one");
stringList.Add("two");
变量racers定义为类型List<Racer>。

使用new操作符创建相同类型的一个新对象。

因为类List<T>用具体类Racer来实例化,所以现在只有Racer对象可以用Add()方法添加。

在下面的例子中,创建了4个一级方程式赛手,并添加到集合中。

List<Racer> racers = new List<Racer>(20);
Racer graham = new Racer("Graham", "Hill", "UK", 14);
Racers.Add(graham);
252
第10章集合
Racer emerson = new Racer("Emerson", "Fittipaldi", "Brazil", 14);
Racers.Add(emerson);
Racer mario = new Racer("Mario", "Andretti", "USA", 12);
Racers.Add(Mario);
racers.Add(new Racer("Michael", "Schumacher", "Germany", 91));
racers.Add(new Racer("Mika", "Hakkinen", "Finland", 20));
使用List<T>类的AddRange()方法,可以一次给集合添加多个元素。

AddRange()方法
的参数是IEnumerable<T>类型的对象,所以也可以传送一个数组,如下所示:
racers.AddRange(new Racer[] {
new Racer("Niki", "Lauda", "Austria", 25)
new Racer("Alian", "Prost", "France", 51 } );
如果在实例化列表时知道集合的元素个数,也可以将执行IEnumerable<T>的任意对象
传送给类的构造函数。

这非常类似于AddRange()方法:
List<Racer> racers = new List<Racer> (new Racer[] {
new Racer("Jochen", "Rindt", "Austria", 6)
new Racer("Ayrton", "Senna", "Brazil", 41 } );
2. 插入元素
使用Insert()方法可以在指定位置插入元素:
racers. Insert(3, new Racer("Phil", "Hill", "USA", 3));
方法InsertRange()提供了插入大量元素的容量,类似于前面的AddRange()方法。

如果索引集大于集合中的元素个数,就抛出ArgumentOutOfRangeException类型的
异常。

3. 访问元素
执行了IList和IList<T>接口的所有类都提供了一个索引器,所以可以使用索引器,通
过传送元素号来访问元素。

第一个元素可以用索引值0来访问。

Racer r1 = racers[3];
可以用Count属性确定元素个数,再使用for循环迭代集合中的每个元素,使用索引
器访问每一项:
for (int i=0; i<racers.Count; i++)
{
Console.WriteLine(racers[i]);
}
提示:
可以通过索引访问的集合类有ArrayList、StringCollection和List<T>。

253
第Ⅰ部分 C# 语言
254 List<T>执行了接口IEnumerable,所以也可以使用foreach语句迭代集合中的元素。

注意:
编译器解析foreach语句时,利用了接口IEnumerable和IEnumerator,参见第5章。

foreach (Racer r in racers)
{
Console.WriteLine(r);
}
除了使用foreach语句之外,List<T>类还提供了ForEach()方法,它用Action<T>参数
声明。

ForEach迭代集合中的每一项,调用传送作为每一项的参数的方法。

public void ForEach(Action<T> action);
为了给ForEach传送一个方法,Action<T>声明为一个委托,它定义了一个返回类型为void、参数为T的方法。

public delegate void Action<T>(T obj);
在Racer项的列表中,ForEach()方法的处理程序必须声明为以Racer对象作为参数,返回类型是void。

public void ActionHandler(Racer obj);
Console.WriteLine()方法的一个重载版本将Object作为参数,所以可以将这个方法的地址传送给ForEach()方法,把集合中的每个赛手写入控制台:
racers.ForEach(Console.WriteLine);
也可以编写一个匿名方法,它将Racer对象作为参数。

这里,格式A由IFormattable 接口的ToString()方法用于显示赛手的所有信息:
racers.ForEach(
delegate(Racer r)
{
Console.WriteLine("{0:A}", r);
});
注意:
匿名方法详见第7章。

4. 删除元素
删除元素时,可以利用索引,或传送要删除的元素。

下面的代码把3传送给RemoveAt(),删除第4个元素:
racers. RemoveAt(3);
第10章集合
也可以直接将Racer对象传送给Remove()方法,删除这个元素。

按索引删除比较快,
因为必须在集合中搜索要删除的元素。

Remove()方法先在集合中搜索,用IndexOf()方法确
定元素的索引,再使用该索引删除元素。

IndexOf()方法先检查元素类型是否执行了IEquatable接口。

如果是,就调用这个接口中的Equals()方法,在集合中查找传送给Equals()
方法的元素。

如果没有执行这个接口,就使用Object类的Equals()方法比较元素。

Object
类的Equals()方法的默认实现代码对值类型进行按位比较,对引用类型只比较其引用。

注意:
第6章介绍了如何重写Equals()方法。

这里从集合中删除了变量graham引用的赛手。

变量graham是前面在填充集合时创建的。

接口IEquatable和Object.Equals()方法都没有在Racer类中重写,所以不能用要删除的
元素内容创建一个新对象,再把它传送给Remove()方法。

If(!racers. Remove(graham))
{
Console.WriteLine("object not found in collection");
}
方法RemoveRange()可以从集合中删除许多元素。

它的第一个参数指定了开始删除的
元素索引,第二个参数指定了要删除的元素个数。

int index = 3;
int count = 5;
racers. RemoveRange (index, count)
要从集合中删除有指定特性的所有元素,可以使用RemoveAll()方法。

这个方法使用
下面搜索元素时将讨论的Predicate<T>参数。

要删除集合中的所有元素,可以使用ICollection<T>接口定义的Clear()方法。

5. 搜索
有不同的方式在集合中搜索元素。

可以获得要查找的元素的索引,或者搜索元素本身。

可以使用的方法有IndexOf()、LastIndexOf()、FindIndex()、FindLastIndex()、Find()和FindLast()。

如果只检查元素是否存在,List<T>类提供了Exists()方法。

方法IndexOf()需要将一个对象作为参数,如果在集合中找到该元素,这个方法就返回
该元素的索引。

如果没有找到该元素,就返回-1。

IndexOf()方法使用IEquatable接口来比
较元素。

int index1 = racers. IndexOf(mario);
使用方法IndexOf(),还可以指定不需要搜索整个集合,但必须指定从哪个索引开始搜
索以及要搜索的元素个数。

除了使用IndexOf()方法搜索指定的元素之外,还可以搜索有某个特性的元素,该特性
255
第Ⅰ部分 C# 语言
可以用FindIndex()方法来定义。

FindIndex()方法需要一个Predicate类型的参数:
public int FindIndex(Predicate<T> match);
Predicate<T>类型是一个委托,它返回一个布尔值,需要把类型T作为参数。

这个委托的用法与ForEach()方法中的Action委托类似。

如果Predicate返回true,就表示有一个匹配,找到了一个元素。

如果它返回false,就表示没有找到元素,搜索将继续。

public delegate bool Predicate<T>(T obj);
在List<T>类中,把Racer对象作为类型T,所以可以将一个方法(该方法将类型Racer 定义为参数、且返回bool)的地址传送给FindIndex()方法。

查找指定国家的第一个赛手时,可以创建如下所示的FindCountry类。

Find()方法的签名和返回类型是通过Predicate<T>委托定义的。

Find()方法使用变量country搜索用FindCountry类的构造函数定义的一个国家。

public class FindCountry
{
public FindCountry(string country)
{
this.country = country;
}
private string country;
public bool FindCountryPredicate(Racer r)
{
if(r == 0) throw new ArgumentNullException("r");
return r.Country == country;
}
}
使用FindIndex()方法可以创建FindCountry()类的一个新实例,把一个国家字符串传送给构造函数,再传送Find方法的地址。

FindIndex()方法成功完成后,index2就包含集合中Country属性设置为Finland的第一项的索引。

int index2 = racers. FindIndex(new FindCountry("Finland"). FindCountryPredicate);
除了用处理程序方法创建一个类之外,还可以在这里创建一个匿名方法。

结果与前面完全相同。

现在委托定义了匿名方法的实现代码,来搜索Country属性设置为Finland的元素。

int index3 = racers. FindIndex(
delegate(Racer r)
{
return r.Country == "Finland";
}
与IndexOf()方法类似,使用FindIndex()方法也可以指定搜索开始的索引和要迭代的元素个数。

为了从集合中最后一个元素开始向前搜索,可以使用FindLastIndex()方法。

方法FindIndex()返回所搜索元素的索引。

除了获得索引之外,还可以直接获得集合中的元素。

方法Find()需要一个Predicate<T>类型的参数,这与FindIndex()方法类似。

下面的
256
方法Find()搜索列表中Firstname属性设置为Niki的第一个赛手。

当然,也可以执行FindLast()
方法,查找与Predicate匹配的最后一项。

Racer r = racers. Find(
delegate(Racer r)
{
return r.Firstname == "Niki";
});
要获得与Predicate匹配的所有项,而不是一项,可以使用FindAll()方法。

FindAll()方
法使用的Predicate<T>委托与Find()和FindIndex()方法相同。

FindAll()方法在找到第一项后,
不会停止搜索,而是迭代集合中的每一项,返回Predicate是true的所有项。

这里调用了FindAll()方法,返回Wins属性设置超过20的所有racer项。

所有赢得20
多场比赛的赛手都从bigWinners列表中引用。

List<Racer> bigWinners = racers.FindAll(
delegate(Racer r)
{
return r.Wins > 20;
}
};
用foreach语句迭代bigWinners变量,结果如下:
foreach(Racer r in bigWinners)
{
Console.WriteLine("{0:A}", r);
}
Michael Schumacher, Germany Wins: 91
Niki Lauda, Austria Wins: 25
Alain Prost, France Wins: 51
这个结果没有排序,但这是下一步要做的工作。

6. 排序
List<T>类可以对元素排序。

Sort()方法定义了几个重载方法。

可以传送给它的参数有泛
型委托Comparison<T>、泛型接口IComparer<T>、泛型接口IComparer<T>和一个范围值。

public void List<T>. Sort();
public void List<T>. Sort(Comparison<T>);
public void List<T>. Sort(IComparer<T>);
public void List<T>. Sort(Int32, Int32, IComparer<T>);
只有集合中的元素执行了接口IComparable,才能使用不带参数的Sort()方法。

类Racer实现了IComparable<T>接口,可以按姓氏对赛手排序:
racers. Sort();
racers.ForEach(Console.WriteLine);
257
如果需要按照元素类型不默认支持的方式排序,就应使用其他技术,例如传送执行了IComparer<T>接口的对象。

类RacerComparer给Racer类型执行了接口IComparer<T>。

这个类允许按名字、姓氏、国籍或获胜人数排序。

排序的种类用内部枚举类型CompareType定义。

CompareType用类RacerComparer的构造函数设置。

接口IComparer<Racer>定义了排序所需的方法Compare()。

在这个方法的实现代码中,使用了string和int类型的CompareTo()方法。

public class RacerComparer : IComparer<Racer>
{
public enum CompareType
{
Firstname,
Lastname;
Country;
Wins
}
private CompareType compareType;
public RacerComparer(CompareType compareType)
{
this. compareType = compareType;
}
public int Compare(Racer x, Racer y)
{
if (x == null) throw new ArgumentNullException("x");
if (y == null) throw new ArgumentNullException("y");
int result;
switch (compareType)
{
case compareType.Firstname:
return pareTo(y.Firstname);
case stname:
return pareTo(stname);
case compareType.Country:
if ((result = pareTo(y. Country) == 0)
return pareTo(stname);
else
return res;
case compareType.Wins:
return pareTo(y.Wins);
default:
throw new ArgumentNullException("Invalid Compare Type");
}
}
}
现在,可以对类RacerComparer的一个实例使用Sort()方法。

传送枚举RacerComparer.
CompareType.Country,按属性Country对集合排序:
258
racers.Sort(new RacerComparer(RacerComparer .CompareType. Country));
racers.ForEach(Console.WriteLine);
排序的另一种方式是使用重载的Sort()方法,它需要一个Comparison<T>委托:
public void List<T> Sort (Comparison<T>);
Comparison<T>是一个方法的委托,该方法有两个T类型的参数,返回类型为int。


果参数值相等,该方法就返回0。

如果第一个参数比第二个小,就返回小于0的值;否则,
返回大于0的值。

public delegate int Comparison<T>(T x, T y);
现在可以把一个简单的匿名方法传送给Sort()方法,按获胜者的人数排序。

两个参数
的类型是Racer,在其实现代码中,使用int方法CompareTo()比较Wins属性。

在实现代码中,r2和r1以逆序方式使用,所以获胜者的人数以降序方式排序。

方法调用完后,完整的
赛手列表就按赛手的姓名排序。

racers.Sort(delegate(Racer r1, Racer r2) {
return pareTo(r1.Wins); });
也可以调用Reverse()方法,倒转整个集合的顺序。

7. 类型转换
使用List<T>类的ConvertAll()方法,可以把任意类型的集合转换为另一种类型。

ConvertAll()方法使用一个Converter委托,其定义如下:
public sealed delegate TOutput Converter<TInput, TOutput>(TInput from);
泛型类型TInput和TOutput用于转换。

TInput是委托方法的变元,TOutput是返回
类型。

在这个例子中,所有的Racer类型都应转换为Person类型。

Racer类型包含姓名和汽车,而Person类型只包含姓名。

为了进行转换,可以忽略赛手的国籍,但姓名必须转换:
[Serializable]
public class Person
{
private string name;
public Person(string name)
{
= name;
}
public override string ToString()
{
return name;
}
}
259
转换时调用了racers.ConvertAll<Person>()方法。

这个方法的变元定义为一个匿名方法,其变元的类型是Racer,返回类型是Person。

在匿名方法的实现代码中,创建并返回了一个新的Person对象。

对于Person对象,把Firstname和Lastname传送给构造函数:
List<Person> persons = racers. ConvertAll<Person>(
delegate(Racer r)
{
return new Person(r.Firstname + " " +stname);
});
转换的结果是一个列表,其中包含转换过来的Person对象:类型为List<Person>的persons列表。

10.2.2 只读集合
集合创建好后,就是可读写的。

当然,集合必须是可读写的,否则就不能给它填充值了。

但是,在填充完集合后,可以创建只读集合。

List<T>集合的方法AsReadOnly返回ReadOnlyCollection<T>类型的对象。

ReadOnlyCollection<T>类执行的接口与List<T>相同,但所有修改集合的方法和属性都抛出NotSupportedException异常。

10.3 队列
队列是其元素以先进先出(FIFO)的方式来处理的集合。

先放在队列中的元素会先读取。

队列的例子有在机场排的队、人力资源部中等待处理求职信的队列、打印队列中等待处理的打印任务、以循环方式等待CPU处理的线程。

另外,还常常有元素根据其优先级来处理的队列。

例如,在机场的队列中,商务舱乘客的处理要优先于经济舱的乘客。

这里可以使用多个队列,一个队列对应一个优先级。

在机场,这是很常见的,因为商务舱乘客和经济舱乘客有不同的登记队列。

打印队列和线程也是这样。

可以为一组队列建立一个数组,数组中的一项代表一个优先级。

在每个数组项中,都有一个队列,其处理按照FIFO的方式进行。

注意:
本章的后面将使用链表的另一种实现方式,来定义优先级列表。

在.NET的System.Collections命名空间中有非泛型类Queue,在System.Collections.
Generic命名空间中有泛型类Queue<T>。

这两个类的功能非常类似,但泛型类是强类型化的,定义了类型T,而非泛型类基于Object类型。

在内部,Queue<T>类使用T类型的数组,这类似于List<T>类型。

另一个类似之处是它们都执行ICollection和IEnumerable接口。

Queue类执行了ICollection、IEnumerable和ICloneable接口。

Queue<T>类没有执行泛型接口ICollection<T>,因为这个接口用Add()和Remove()方法定义了在集合中添加和删除元素的方法。

260
队列与列表的主要区别是队列没有执行IList接口。

所以不能用索引器访问队列。

队列
只允许添加元素,该元素会放在队列的尾部(使用Enqueue()方法),从队列的头部获取元素(使用Dequeue()方法)。

图10-1显示了队列的元素。

Enqueue()方法在队列的一端添加元素,Dequeue()方法在
队列的另一端读取和删除元素。

用Dequeue()方法读取元素,将同时从队列中删除该元素。

再调用一次Dequeue()方法,会删除队列中的下一项。

图 10-1
Queue和Queue <T>类的方法如表10-2所示。

在创建队列时,可以使用与List<T>类型类似的构造函数。

默认的构造函数会创建一
个空队列,也可以使用构造函数指定容量。

在把元素添加到队列中时,容量会递增,包含
4、8、16和32个元素。

与List<T>类型类似,队列的容量也总是根据需要成倍增加。

非泛
型类Queue的默认构造函数与此不同,它会创建一个包含32项的空数组。

使用构造函数
的重载版本,还可以将执行了IEnumerable<T>接口的其他集合复制到队列中。

下面的文档管理应用程序示例演示了Queue<T>类的用法。

使用一个线程将文档添加
到队列中,用另一个线程从队列中读取文档,并处理它们。

存储在队列中的项是Document类型。

Document类定义了标题和内容:
public class Document
{
261
private string title;
public string Title
{
get
{
return title;
}
}
private string content;
public string Content
{
get
{
return content;
}
}
public Document(string title, string content)
{
this.title = title;
this.content = content;
}
}
DocumentManager类是Queue<T>类外面的一层。

DocumentManager类定义了如何处理文档:用AddDocument()方法将文档添加到队列中,用GetDocument()方法从队列中获得文档。

在AddDocument()方法中,用Enqueue()方法把文档添加到队列的尾部。

在GetDocument()方法中,用Dequeue()方法从队列中读取第一个文档。

多个线程可以同时访问DocumentManager,所以用lock语句锁定对队列的访问。

提示:
线程和lock语句参见第18章。

IsDocumentAvailable是一个只读布尔属性,如果队列中还有文档,它就返回true,否则返回false。

public class DocumentManager
{
private readonly Queue<Document> documentQueue = new Queue<Document>;
public void AddDocument(Document doc)
{
lock(this)
{
documentQueue.Enqueue(doc);
}
262
}
public Document GetDocument()
{
Document doc = null;
lock(this)
{
doc = documentQueue.Dequeue();
}
return doc;
}
public bool IsDocumentAvailable
{
get
{
return documentQueue.Count > 0;
}
}
}
类ProcessDocuments在一个单独的线程中处理队列中的文档。

能从外部访问的唯一方
法是Start()。

在Start()方法中,实例化了一个新线程。

创建一个ProcessDocuments对象,
来启动线程,定义Run()方法作为线程的启动方法。

ThreadStart是一个委托,它引用了由
线程启动的方法。

在创建Thread对象后,就调用Thread.Start()方法来启动线程。

使用ProcessDocuments类的Run()方法定义一个无限循环。

在这个循环中,使用属性IsDocumentAvailable确定队列中是否还有文档。

如果还有,就从DocumentManager中提取
文档并处理。

这里的处理仅是把信息写入控制台。

在真正的应用程序中,文档可以写入文件、数据库,或通过网络发送。

public class ProcessDocuments
{
public static void Start(DocumentManager dm)
{
new Thread(new ProcessDocuments(dm).Run).Start();
}
protected ProcessDocuments(DocumentManager dm)
{
documentManager = dm;
}
private DocumentManager documentManager;
protected void Run()
{
while (true)
{
if (documentManager.IsDocumentAvailable)
263
{
Document doc = documentManager.GetDocument();
Console.WriteLine("Processing document {0}", doc.Title);
}
Thread.Sleep(new Random().Next(20));
}
}
}
在应用程序的Main()方法中,实例化一个DocumentManager对象,启动文档处理线程。

接着创建1000个文档,并添加到DocumentManager中:
class Program
{
static void Main
{
DocumentManager dm = new DocumentManager();
ProcessDocuments.Start(dm);
// Create documents and add them to the DocumentManager
for (int i = 0; i < 1000; i++)
{
Document doc = new Document("Doc " + i.ToString(), "content");
dm.AddDocument(doc);
Console.WriteLine("added document {0}", doc.Title);
Thread.Sleep(new Random().Next(20));
}
}
}
在启动应用程序时,会在队列中添加和删除文档,输出如下所示:
Added document Doc 279
Processing document Doc 236
Added document Doc 280
Processing document Doc 237
Added document Doc 281
Processing document Doc 238
Processing document Doc 239
Processing document Doc 240
Processing document Doc 241
Added document Doc 282
Processing document Doc 242
Added document Doc 283
Processing document Doc 243
完成示例应用程序中描述的任务的真实程序可以处理用Web服务接收到的文档。

264
10.4 栈
栈是与队列非常类似的另一个容器,只是要使用不同的方法访问栈。

最后添加到栈中
的元素会最先读取。

栈是一个后进先出(LIFO)容器。

图10-2表示一个栈,用Push()方法在栈中添加元素,用Pop()方法获取最近添加的
元素。

图 10-2
与Queue类相同,非泛型类Statck也执行了ICollection、IEnumerable和ICloneable接口;泛型类Statck<T>实现了IEnumerable<T>、ICollection和IEnumerable接口。

Statck和Statck<T>类的成员如表10-3所示。

在下面的例子中,使用Push()方法把三个元素添加到栈中。

在foreach方法中,使用IEnumerable接口迭代所有的元素。

栈的枚举器不会删除元素,只会逐个返回元素。

Stack<char> alphabet = new Stack<char>();
265
alphabet.Push('A');
alphabet.Push('B');
alphabet.Push('C');
foreach (char item in alphabet)
{
Console.Write(item);
}
Console.WriteLine();
因为元素的读取顺序是从最后一个添加到栈中的元素开始到第一个元素,所以得到的结果如下:
CBA
用枚举器读取元素不会改变元素的状态。

使用Pop()方法会从栈中读取每个元素,然后删除它们。

这样,就可以使用while循环迭代集合,检查Count属性,确定栈中是否还有元素:
Stack<char> alphabet = new Stack<char>();
alphabet.Push('A');
alphabet.Push('B');
alphabet.Push('C');
Console.Write("First iteration: ");
foreach (char item in alphabet)
{
Console.Write(item);
}
Console.WriteLine();
Console.Write("Second iteration: ");
while (alphabet.Count > 0)
{
Console.Write(alphabet.Pop());
}
Console.WriteLine();
结果是两个CBA。

在第二次迭代后,栈变空,因为第二次迭代使用了Pop()方法:
First iteration: CBA
Second iteration: CBA
10.5 链表
LinkedList<T>集合类没有非泛型集合的类似版本。

LinkedList<T>是一个双向链表,其元素指向它前面和后面的元素,如图10-3所示。

266
267
图 10-3
链表的优点是,如果将元素插入列表的中间位置,使用链表会非常快。

在插入一个元素时,只需修改上一个元素的Next 引用和下一个元素的Previous 引用,使它们引用所插入的元素。

在List<T>和ArrayList 类中,插入一个元素,需要移动该元素后面的所有元素。

当然,链表也有缺点。

链表的元素只能一个接一个地访问,这需要较长的时间来查找位于链表中间或尾部的元素。

链表不仅能在列表中存储元素,还可以给每个元素存储下一个元素和上一个元素的信息。

这就是LinkedList<T>包含LinkedListNode<T>类型的元素的原因。

使用LinkedListNode<T>类,可以获得列表中的下一个元素和上一个元素。

表10-4描述了LinkedListNode<T>的属性。

类LinkedList<T>执行了IEnumerable<T>、ICollection<T>、ICollection 、IEnumerable 、ISerializable 和IDeserializationCallback 接口。

这个类的成员如表10-5所示。

示例应用程序使用了一个链表LinkedList<T>和一个列表List<T>。

链表包含文档,这与上一个例子相同,但文档有一个额外的优先级。

在链表中,文档按照优先级来排序。

如果多个文档的优先级相同,则这些元素就按照文档的插入时间来排序。

图10-4描述了示例应用程序中的集合。

LinkedList<Document>是一个包含所有Document对象的链表,该图显示了文档的标题和优先级。

标题指出了文档添加到链表中的时间。

第一个添加的文档的标题是One。

第二个添加的文档的标题是Two,依此类推。

可以看出,文档One和Four有相同的优先级8,因为One在Four之前添加,所以One放在链表的前面。

在链表中添加新文档时,它们应放在优先级相同的最后一个文档后面。

集合LinkedList <Document>包含LinkedListNode<Document>类型的元素。

类LinkedListNode<T>添加Next 和Previous属性,使搜索过程能从一个节点移动到下一个节点上。

要引用这类元素,应把List<T>定义为List<LinkedListNode<Document>>。

为了快速访问每个优先级的最后一个文档,集合List<LinkedListNode>应最多包含10个元素,每个元素都引用不同优先级的最后一个文档。

在后面的讨论中,对不同优先级的最后一个文档的引用称为优先级节点。

268。

相关文档
最新文档