集合

 

 

 集合

 

第5章介绍了数组和Array类执行的接口。数组的大小是固定的。如果元素个数是动态的,就应使用集合类。

List<T>和ArrayList是与数组相当的集合类。还有其他类型的集合:队列、栈、链表和字典。

本章介绍如何使用对象组。主要内容如下:

●       集合接口和类型

●       列表

●       队列

●       栈

●       链表

●       有序表

●       字典

●       带多个键的字典

●       位数组

●       性能

10.1 集合接口和类型

集合类可以组合为集合,存储Object类型的元素和泛型集合类。在.NET 2.0之前,不存在泛型。现在泛型集合类通常是集合的首选类型。泛型集合类是类型安全的,如果使用值类型,是不需要装箱操作的。如果要在集合中添加不同类型的对象,且这些对象不是相互派生的,例如在集合中添加int和string对象,就只需基于对象的集合类。另一组集合类是专用于特定类型的集合,例如StringCollection类专用于string类型。

提示:

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

对象类型的集合位于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-1

接    口

方法和属性

说    明

IEnumerable,

IEnumerable<T>

GetEnumerator()

如果将foreach语句用于集合,就需要接口IEnumerable。这个接口定义了方法GetEnumerator(),它返回一个实现了IEnumerator的枚举。泛型接口IEnumerable<T>继承了非泛型接口IEnumerable,定义了一个返回Enumerable<T>的GetEnumerator方法。因为这两个接口具有继承关系,所以对于每个需要IEnumerable类型参数的方法,都可以传送Enumerable<T>对象

ICollection

Count,

IsSynchronized,

SyncRoot,

CopyTo()

接口ICollection由集合类实现。使用这个接口的方法可以在集合中添加和删除元素

ICollection<T>

Count,IsReadOnly,

Add(),Clear(),

Contains(),CopyTo()

Remove()

接口ICollection<T>扩展了接口IEnumerable的功能

IList

IsFixedSize,

IsReadOnly,

Item,Add,Clear,

Contains,IndexOf,

Insert,Remove,

RemoveAt

接口IList派生于接口ICollection。IList允许使用索引器访问集合,还可以在集合的任意位置插入或删除元素

 

IList<T>

Item,IndexOf

Insert,Remove

与接口ICollection<T>类似,接口IList<T>也继承了接口ICollection。

第5章提到,Array类实现了这个接口,但添加或删除元素的方法会抛出NotSupportedException异常。在大小固定的只读集合(如Array类)中,这个接口定义的一些方法会抛出NotSupportedExceptiuon异常

(续表)  

接口

方法和属性

说明

IDictionary

IsFixedSize,

IsReadOnly,Item,

Keys,Values,Add(),Clear(),Contains(),

GetEnumerator(),

Remove()

接口IDictionary或IDictionary<TKey,TValue>由其元素包含键和值的集合实现

IDictionary<TKey,TValue>

Item,Keys,Values,Add(),

ContainsKey (),

Remove(),

TyrGetValue()

 

IComparer<T>

Compare()

接口IComparer<T>由比较器实现,通过Compare()方法给集合中的元素排序

IEqualityComparer<T>

Equals(),

GetHashCode()

接口IEqualityComparer<T>由一个比较器实现,该比较器可用于字典中的键。使用这个接口,可以对对象进行相等比较。方法GetHashCode()应为每个对象返回一个唯一值。如果对象相等,Equals()方法就返回true,否则返回false

 

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

{

   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;

       this.lastname = 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)

       {

           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 this.lastname. CompareTo(other.lastname);

}

}

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()将旧数组中的元素复制到新数组中。为节省时间,如果事先知道列表中元素的个数,就可以用构造函数定义其容量。下面创建了一个容量为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);

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>。

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

也可以直接将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()方法搜索指定的元素之外,还可以搜索有某个特性的元素,该特性可以用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()方法类似。下面的方法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);

如果需要按照元素类型不默认支持的方式排序,就应使用其他技术,例如传送执行了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 x.Firstname.CompareTo(y.Firstname);

   case compareType.Lastname:

     return x.Lastname.CompareTo(y.Lastname);

   case compareType.Country:

     if ((result = x.Country.CompareTo(y. Country) == 0)

        return x.Lastname.CompareTo(y.Lastname);

               else

                    return res;

   case compareType.Wins:

               return x.Wins.CompareTo(y.Wins);

           default:

throw new ArgumentNullException("Invalid Compare Type");

        }

}

}

现在,可以对类RacerComparer的一个实例使用Sort()方法。传送枚举RacerComparer. CompareType.Country,按属性Country对集合排序:

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 r2.Wins.CompareTo(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)

    {

    this.name = name;

}

 

public override string ToString()

{

        return name;

}

}

转换时调用了racers.ConvertAll<Person>()方法。这个方法的变元定义为一个匿名方法,其变元的类型是Racer,返回类型是Person。在匿名方法的实现代码中,创建并返回了一个新的Person对象。对于Person对象,把Firstname和Lastname传送给构造函数:

List<Person> persons = racers. ConvertAll<Person>(

   delegate(Racer r)

   {

        return new Person(r.Firstname + " " +r.Lastname);

});

转换的结果是一个列表,其中包含转换过来的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()方法定义了在集合中添加和删除元素的方法。

队列与列表的主要区别是队列没有执行IList接口。所以不能用索引器访问队列。队列只允许添加元素,该元素会放在队列的尾部(使用Enqueue()方法),从队列的头部获取元素(使用Dequeue()方法)。

图10-1显示了队列的元素。Enqueue()方法在队列的一端添加元素,Dequeue()方法在队列的另一端读取和删除元素。用Dequeue()方法读取元素,将同时从队列中删除该元素。再调用一次Dequeue()方法,会删除队列中的下一项。

图 10-1

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

表 10-2

Queue和Queue <T>类的成员

说    明

Enqueue()

在队列一端添加一个元素

Dequeue()

在队列的头部读取和删除一个元素。如果在调用Dequeue()方法时,队列中不再有元素,就抛出InvalidOperationException异常

Peek()

在队列的头部读取一个元素,但不删除它

Count

返回队列中的元素个数

TrimExcess()

重新设置队列的容量。Dequeue()方法从队列中删除元素,但不会重新设置队列的容量。要从队列的头部去除空元素,应使用TrimExcess()方法

Contains()

确定某个元素是否在队列中,如果是,就返回true

CopyTo()

ToArray()

把元素从队列复制到一个已有的数组中。ToArray()方法返回一个包含队列元素的新数组

 

在创建队列时,可以使用与List<T>类型类似的构造函数。默认的构造函数会创建一个空队列,也可以使用构造函数指定容量。在把元素添加到队列中时,容量会递增,包含4、8、16和32个元素。与List<T>类型类似,队列的容量也总是根据需要成倍增加。非泛型类Queue的默认构造函数与此不同,它会创建一个包含32项的空数组。使用构造函数的重载版本,还可以将执行了IEnumerable<T>接口的其他集合复制到队列中。

下面的文档管理应用程序示例演示了Queue<T>类的用法。使用一个线程将文档添加到队列中,用另一个线程从队列中读取文档,并处理它们。

存储在队列中的项是Document类型。Document类定义了标题和内容:

public class Document

{

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

}

}

 

    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)

            {

               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服务接收到的文档。

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所示。

表 10-3

Statck和Statck<T>类的成员

说    明

Push()

在栈顶添加一个元素

Pop()

从栈顶删除一个元素,并返回该元素。如果栈是空的,就抛出InvalidOperationException异常

Peek()

返回栈顶元素,但不删除它

Count

返回栈中的元素个数

Contains()

确定某个元素是否在栈中,如果是,就返回true

CopyTo()

ToArray()

把元素从栈复制到一个已有的数组中。ToArray()方法返回一个包含栈中元素的新数组

 

在下面的例子中,使用Push()方法把三个元素添加到栈中。在foreach方法中,使用IEnumerable接口迭代所有的元素。栈的枚举器不会删除元素,只会逐个返回元素。

Stack<char> alphabet = new Stack<char>();

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所示。

图 10-3

链表的优点是,如果将元素插入列表的中间位置,使用链表会非常快。在插入一个元素时,只需修改上一个元素的Next引用和下一个元素的Previous引用,使它们引用所插入的元素。在List<T>和ArrayList类中,插入一个元素,需要移动该元素后面的所有元素。

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

链表不仅能在列表中存储元素,还可以给每个元素存储下一个元素和上一个元素的信息。这就是LinkedList<T>包含LinkedListNode<T>类型的元素的原因。使用LinkedListNode<T>类,可以获得列表中的下一个元素和上一个元素。表10-4描述了LinkedListNode<T>的属性。

表 10-4

LinkedListNode<T>的属性

说    明

List

返回与节点相关的LinkedList<T>

Next

返回当前节点之后的节点。其返回类型是LinkedListNode<T>

Previous

返回当前节点之前的节点

Value

返回与节点相关的元素,其类型是T

 

类LinkedList<T>执行了IEnumerable<T>、ICollection<T>、ICollection、IEnumerable、ISerializable和IDeserializationCallback接口。这个类的成员如表10-5所示。

表 10-5

LinkedList<T>的成员

说    明

Count

返回链表中的元素个数

First

返回链表中的第一个节点。其返回类型是LinkedListNode<T>。使用这个返回的节点,可以迭代集合中的其他节点

Last

返回链表中的最后一个元素。其返回类型是LinkedListNode<T>

AddAfter()

AddBefore()

AddFirst()

AddLast()

使用AddXXX方法可以在链表中添加元素。使用相应的Add方法,可以在链表的指定位置添加元素。AddAfter()需要一个LinkedListNode<T>对象,在该对象中可以指定要添加的新元素后面的节点。AddBefore()把新元素放在第一个参数定义的节点前面。AddFirst()和AddLast()把新元素添加到链表的开头和结尾

重载所有这些方法来接收任一个对象以添加类型LinkedListNode<T>或类型T。如果传送T对象,则创建一个新LinkedListNode<T>对象

Remove()

RemoveFirst()

RemoveLast()

Remove()、RemoveFirst()和RemoveLast()方法从链表中删除节点。RemoveFirst()删除第一个元素,RemoveLast()删除最后一个元素。Remove()需要搜索一个对象,从链表中删除匹配该对象的第一个节点

Clear()

从链表中删除所有的节点

Contains()

在链表中搜索一个元素,如果找到该元素,就返回true,否则返回false

Find()

从链表的开头开始搜索传送给它的元素,并返回一个LinkedListNode<T>

FindLast()

与Find()方法类似,但从链表的结尾开始搜索

示例应用程序使用了一个链表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个元素,每个元素都引用不同优先级的最后一个文档。在后面的讨论中,对不同优先级的最后一个文档的引用称为优先级节点。

图 10-4

在上面的例子中,Document类扩展为包含优先级。优先级用类的构造函数设置:

   public class Document

   {

      private string title;

      public string Title

      {

         get

         {

            return title;

         }

      }

 

      private string content;

      public string Content

      {

         get

         {

            return content;

         }

      }

 

      private byte priority;

      public byte Priority

      {

         get

         {

            return priority;

         }

      }

 

      public Document(string title, string content, byte priority)

      {

         this.title = title;

         this.content = content;

         this.priority = priority;

      }

   }

解决方案的核心是PriorityDocumentManager类。这个类很容易使用。在这个类的公共接口中,可以把新的Document元素添加到链表中,检索第一个文档,为了便于测试,它还提供了一个方法,在元素链接到链表中时,该方法可以显示集合中的所有元素。

PriorityDocumentManager类包含两个集合。LinkedList<Document>类型的集合包含所有的文档。List<LinkedListNode<Document>>类型的集合包含最多10个元素的引用,它们是添加指定优先级的新文档的入口点。这两个集合变量都用PriorityDocumentManager类的构造函数来初始化。列表集合也用null初始化:

   public class PriorityDocumentManager

   {

      private readonly LinkedList<Document> documentList;

 

      // priorities 0..9

      private readonly List<LinkedListNode<Document>> priorityNodes;

 

      public PriorityDocumentManager()

      {

         documentList = new LinkedList<Document>();

 

         priorityNodes = new List<LinkedListNode<Document>>(10);

         for (int i = 0; i < 10; i++)

         {

            priorityNodes.Add(new LinkedListNode<Document>(null));

         }

      }

在类的公共接口中,有一个方法AddDocument()。它只是调用私有方法AddDocumentTo- PriorityNode()。把实现代码放在另一个方法中的原因是,AddDocumentToPriorityNode()可以递归调用,如后面所示。

      public void AddDocument(Document d)

      {

          if (d == null) throw new ArgumentNullException("d");

AddDocumentToPriorityNode(d, d.Priority);

      }

在AddDocumentToPriorityNode()的实现代码中,第一个操作是检查优先级是否在允许的范围内。这里允许的范围是0~9。如果传送了错误的值,就会抛出ArgumentException类型的异常。

接着检查是否已经有一个优先级节点与所传送的优先级相同。如果在列表集合中没有这样的优先级节点,就递归调用AddDocumentToPriorityNode(),递减优先级值,检查是否有低一级的优先级节点。

如果优先级节点的优先级值与所传送的优先级值不同,也没有比该优先级值更低的优先级节点,就可以调用AddLast()方法,将文档安全地添加到链表的尾部。另外,链表节点由负责指定文档优先级的优先级节点引用。

如果存在这样的优先级节点了,就可以在链表中找到插入文档的位置。这里必须区分是存在指定优先级值的优先级节点,还是引用文档的优先级节点有较低的优先级值。对于第一种情况,可以把新文档插入由优先级节点引用的位置后面。因为优先级节点总是引用指定优先级值的最后一个文档,所以必须设置优先级节点的引用。如果引用文档的优先级节点有较低的优先级值,情况会比较复杂。这里新文档必须插入优先级值与优先级节点相同的所有文档的前面。为了找到优先级值相同的第一个文档,要通过一个while循环,使用Previous属性迭代所有的链表节点, 直到找到一个优先级值不同的链表节点为止。这样,就找到了文档的插入位置,并可以设置优先级节点。

      private void AddDocumentToPriorityNode(Document doc, int priority)

      {

         if (priority > 9 || priority < 0)

            throw new ArgumentException("Priority must be between 0 and 9");

 

         if (priorityNodes[priority].Value == null)

         {

            priority--;

            if (priority >= 0)

            {

               // check for the next lower priority

               AddDocumentToPriorityNode(doc, priority);

            }

            else // now no priority node exists with the same priority or lower

                 // add the new document to the end

            {

               documentList.AddLast(doc);

               priorityNodes[doc.Priority] = documentList.Last;

            }

            return;

         }

         else // a priority node exists

         {

            LinkedListNode<Document> priorityNode = priorityNodes[priority];

            if (priority == doc.Priority)     // priority node with the same

                                                   // priority exists

            {

               documentList.AddAfter(priorityNode, doc);

 

               // set the priority node to the last document with the same priority

               priorityNodes[doc.Priority] = priorityNode.Next;

            }

            else // only priority node with a lower priority exists

            {

               // get the first node of the lower priority

               LinkedListNode<Document> firstPriorityNode = priorityNode;

               while (firstPriorityNode.Previous != null &&

                      firstPriorityNode.Previous.Value.Priority ==

                            priorityNode.Value.Priority)

               {

                  firstPriorityNode = priorityNode.Previous;

               }

 

               documentList.AddBefore(firstPriorityNode, doc);

 

               // set the priority node to the new value

               priorityNodes[doc.Priority] = firstPriorityNode.Previous;

            }

         }

      }

现在还剩下一个简单的方法没有讨论。DisplayAllNodes()只是在一个foreach循环中,把每个文档的优先级和标题显示在控制台上。

GetDocument方法从链表中返回第一个文档(优先级最高的文档),并从链表中删除它:

      public void DisplayAllNodes()

      {

         foreach (Document doc in documentList)

         {

            Console.WriteLine("priority: {0}, title {1}", doc.Priority, doc.Title);

         }

      }

 

      // returns the document with the highest priority (that's first

      // in the linked list)

      public Document GetDocument()

      {

         Document doc = documentList.First.Value;

         documentList.RemoveFirst();

         return doc;

      }

   }

PriorityDocumentManager的Main()方法用于演示其功能。在链表中添加8个优先级不同的新文档,再显示整个链表:

      static void Main

      {

         PriorityDocumentManager pdm = new PriorityDocumentManager();

         pdm.AddDocument(new Document("one", "Sample", 8));

         pdm.AddDocument(new Document("two", "Sample", 3));

         pdm.AddDocument(new Document("three", "Sample", 4));

         pdm.AddDocument(new Document("four", "Sample", 8));

         pdm.AddDocument(new Document("five", "Sample", 1));

         pdm.AddDocument(new Document("six", "Sample", 9));

         pdm.AddDocument(new Document("seven", "Sample", 1));

         pdm.AddDocument(new Document("eight", "Sample", 1));

 

         pdm.DisplayAllNodes();

       }

在处理好的结果中,文档先按优先级排序,再按添加文档的时间排序:

priority: 9, title six

priority: 8, title one

priority: 8, title four

priority: 4, title three

priority: 3, title two

priority: 1, title five

priority: 1, title seven

priority: 1, title eight

10.6 有序表

如果需要排好序的表,可以使用SortedList<TKey, TValue>。这个类按照键给元素排序。

下面的例子创建了一个有序表,其中键和值都是string类型。默认的构造函数创建了一个空表,再用Add()方法添加两本书。使用重载的构造函数,可以定义有序表的容量,传送执行了IComparer<TKey>接口的对象,用于给有序表中的元素排序。

Add()方法的第一个参数是键(书名),第二个参数是值(ISBN号)。除了使用Add()方法之外,还可以使用索引器将元素添加到有序表中。索引器需要把键作为索引参数。如果键已存在,那么Add()方法就抛出一个ArgumentExeption类型的异常。如果索引器使用相同的键,就用新值替代旧值。

SortedList<string, string> books = new SortedList<string, string>();

books.Add(".NET 2.0 Wrox Box", "978-0-470-04840-5");

books.Add("Professional C# 2005", "978-0-7645-7534-1");

 

books["Beginning Visual C# 2005"] = "978-0-7645-4382-1";

books["Professional C# with .NET 3.0"] = "978-0-470-12472-7";

可以使用foreach语句迭代有序表。枚举器返回的元素是KeyValuePair<TKey, TValue>类型,其中包含了键和值。键可以用Key属性访问,值用Value属性访问。

foreach (KeyValuePair<string, string> book in books)

{

Console.WriteLine("{0}, {1}", book.Key, book.Value);

}

迭代语句会按键的顺序显示书名和ISBN号:

.NET 2.0 Wrox Box, 978-0-470-04840-5

Beginning Visual C# 2005, 978-0-7645-4382-1

Professional C# 2005, 978-0-7645-7534-1

Professional C# with .NET 3.0, 978-0-470-12472-7

也可以使用Values和Keys属性访问值和键。Values属性返回IList<TValue>,Keys属性返回IList<TKey>,所以,可以在foreach中使用这些属性:

foreach (string isbn in books.Values)

{

Console.WriteLine(isbn);

}

 

foreach (string title in books.Keys)

{

Console.WriteLine(title);

}

第一个循环显示值,第二个循环显示键:

978-0-470-04840-5

978-0-7645-4382-1

978-0-7645-7534-1

978-0-470-12472-7

.NET 2.0 Wrox Box

Beginning Visual C# 2005

Professional C# 2005

Professional C# with .NET 3.0

SortedList<TKey, TValue>类的属性如表10-6所示。

表 10-6

SortedList的属性

说    明

Capacity

使用Capacity属性可以获取和设置有序表能包含的元素个数。该属性与List<T>类似:默认构造函数会创建一个空表,添加第一个元素会使有序表的容量变成4个元素,之后其容量会根据需要成倍增加

Comparer

Comparer属性返回与有序表相关的比较器。可以在构造函数中传送该比较器。默认的比较器会调用IComparable<TKey>接口的CompareTo()方法来比较键。键类型执行了这个接口,也可以创建定制的比较器

Count

Count属性返回有序表中的元素个数

Item

使用索引器可以访问有序表中的元素。索引器的参数类型由键类型定义

Keys

Keys属性返回包含所有键的IList<TKey>

Values

Values属性返回包含所有值的IList<TValue>

 

SortedList<T>类型的方法类似于本章前面介绍的其他集合。见表10-7。区别是SortedList<T>需要一个键和一个值。

表 10-7

SortedList的方法

说    明

Add()

把带有键和值的元素放在有序表中

Remove(),RemoveAt()

Remove()方法需要从有序表中删除的元素的键。使用RemoveAt()方法可以删除指定索引的元素

Clear()

删除有序表中的所有元素

ContainsKey(),

ContainsValue()

ContainsKey()和ContainsValue()方法检查有序表是否包含指定的键或值,并返回true或false

IndexOfKey(),

IndexOfValue()

IndexOfKey()和IndexOfValue()方法检查有序表是否包含指定的键或值,并返回基于整数的索引

TrimExcess()

重新设置集合的大小,将容量改为需要的元素个数

TryGetValue()

使用TryGetValue()方法可以尝试获得指定键的值。如果键不存在,这个方法就返回false。如果键存在,就返回true,并把值返回为out参数

注意:

除了泛型SortedList<TKey, TValue>之外,还有一个对应的非泛型有序表SortedList。

10.7 字典

字典表示一种非常复杂的数据结构,这种数据结构允许按照某个键来访问元素。字典也称为映射或散列表。字典的主要特性是能根据键快速查找值。也可以自由添加和删除元素,这有点像List<T>,但没有在内存中移动后续元素的性能开销。

图10-5是字典的一个简化表示。其中employee-id,如B4711,是添加到字典中的键。键会转换为一个散列。利用散列创建一个数字,它将索引和值关联起来。然后索引包含一个到值的链接。该图做了简化处理,因为一个索引项可以关联多个值,索引可以存储为一个树形结构。

.NET Framework提供了几个字典类。可以使用的最主要的类是Dictionary<TKey, TValue>。这个类的属性和方法与上面的SortedList<TKey, TValue>几乎完全相同,这里不再赘述。

10.7.1 键的类型

用作字典中键的类型必须重写Object类的GetHashCode()方法。只要字典类需要确定元素的位置,就要调用GetHashCode()方法。GetHashCode()方法返回的int由字典用于计算放置元素的索引。这里不介绍这个算法。我们只需知道,它涉及到素数,所以字典的容量是一个素数。

图 10-5

GetHashCode()方法的实现代码必须满足如下要求:

●       相同的对象应总是返回相同的值。

●       不同的对象可以返回相同的值。

●       应执行得比较快,计算的开销不大。

●       不能抛出异常。

●       应至少使用一个实例字段。

●       散列码值应平均分布在int可以存储的整个数字区域上。

●       散列码最好在对象的生存期中不发生变化。

提示:

字典的性能取决于GetHashCode()方法的实现代码。

为什么要使散列码值平均分布在整数的取值区域内?如果两个键返回的散列会得到相同的索引,则字典类就必须寻找最近的可用自由单元来存储第二个数据项,这需要进行一定的搜索,以便以后检索这一项。显然这会降低性能,如果许多键都有相同的索引,这类冲突就比较多。根据Microsoft的部分算法的工作方式,计算出来的散列值平均分布在int.MinValue和 int.MaxValue之间时,这种风险会降低到最小。

除了实现GetHashCode()方法之外,键类型还必须执行IEquality.Equals()方法,或重写Object类的Equals()方法。因为不同的键对象可能返回相同的散列码,所以字典使用Equals()方法来比较键。字典检查两个键A和B是否相等,并调用A.Equals(B)。这表示必须确保下述条件总是成立:

提示

如果A.Equals(B)返回true,则A.GetHashCode()和B.GetHashCode()必须总是返回相同的散列码。

这似乎有点奇怪,但它非常重要。如果设计出某种重写这些方法的方式,使上面的条件并不总是成立,把这个类的实例用作键的字典就不能正常工作,而是会发生可笑的事情。例如,把一个对象放在字典中后,就再也找不到它,或者试图查找某项,却返回了错误的项。

注意:

因此,如果为Equals()方法提供了重写版本,但没有提供GetHashCode()的重写版本,C#编译器就会显示一个编译警告。

对于System.Object,这个条件为true,因为Equals()方法只是比较引用,GetHashCode()总是返回一个仅基于对象地址的散列。这说明,如果散列表基于一个键,而该键没有重写这些方法,这个散列表就能正常工作。但是,这么做的问题是,只有对象完全相同,键才被认为是相等的。也就是说,把一个对象放在字典中时,必须将它与该键的引用关联起来。也不能在以后用相同的值实例化另一个键对象。如果没有重写Equals()和GetHashCode(),类型在字典中使用时就不太方便。

另外,System.String执行了IEquatable接口,并重载了GetHashCode()方法。Equals()提供了值的比较,GetHashCode()根据字符串的值返回一个散列。因此,字典中把字符串用作键是非常方便的。

数字类型,如Int32,也执行了IEquatable接口,并重载了GetHashCode()方法。但是这些类型返回的散列码只是映射到值上。如果希望用作键的数字本身没有分布在可能的整数值区域内,把整数用作键就不能满足键值平均分布、获得最佳性能的规则。Int32并不适合在字典中使用。

如果所使用的键类型没有执行IEquatable接口,并根据存储在字典中的键值重载GetHashCode(),就可以创建一个执行了IEqualityComparer<T>接口的比较器。IEquality Comparer<T>接口定义了GetHashCode()和Equals()方法,并将对象作为参数,因此可以提供与对象类型不同的实现方式。Dictionary<TKey, TValue>构造函数的一个重载版本允许传送实现了IEqualityComparer <T>接口的对象。如果把这个对象赋予字典,该类就用于生成散列码,比较键。

10.7.2 字典示例

字典示例程序建立了一个员工字典。该字典是用EmployeeId对象来索引的,存储在字典中的每个数据项都是一个Employee对象,该对象存储员工的详细数据。

EmployeeId类定义了在字典中使用的键,该类的成员是表示员工的一个前缀字符和一个数字。这两个变量都是只读的,只能在构造函数中初始化。字典中的键不应改变,这是必须保证的。字段在构造函数中填充。ToString()方法重载为获得员工ID的字符串表示。与键类型的要求一样,EmployeeId也要执行IEquatable接口,重写GetHashCode()方法。

[Serializable]

public class EmployeeId : IEquatable<EmployeeId>

{

private readonly char prefix;

private readonly int number;

public EmployeeId(string id)

{

if (id == null) throw new ArgumentNullException("id");

prefix = (id.ToUpper())[0];

int numLength = id.Length - 1;

number = int.Parse(id.Substring(1, numLength > 6 ? 6 : numLength));

}

public override string ToString()

{

return prefix.ToString() + string.Format("{0,6:000000}", number);

}

public override int GetHashCode()

{

return (number ^ number << 16) * 0x15051505;

}

public bool Equals(EmployeeId other)

{

if (other == null) return false;

return (prefix == other.prefix && number == other.number);

}

}

由IEquatable<T>接口定义的Equals()方法比较两个EmployeeId对象的值,如果这两个值相同,就返回true。除了执行IEquatable<T>接口中的Equals()方法之外,还可以重写Object类中的Equals()方法。

public bool Equals(EmployeeId other)

{

if (other == null) return false;

return (prefix == other.prefix && number == other.number);

}

由于数字是可变的,因此员工可以有1~190000之间的一个值。这并没有填满整数取值区域。GetHashCode()使用的算法将数字向左移动16位,再与原来的数字进行异或操作,最后将结果乘以十六进制数15051505。散列码在整数取值区域上的分布相当均匀:

public override int GetHashCode()

{

return (number ^ number << 16) * 0x15051505;

}

注意:

在Internet上,有许多更复杂的算法,能使散列码在整数取值区域上更好地分布。也可以使用字符串的GetHashCode()方法来返回散列。

Employee类是一个简单的实体类,包含员工的姓名、薪水和ID。构造函数初始化了所有的值,方法ToString()返回一个实例的字符串表示。ToString()方法的实现代码使用StringBuilder对象创建字符串表示,以提高性能。

[Serializable]

public class Employee

{

private string name;

private decimal salary;

private readonly EmployeeId id;

public Employee(EmployeeId id, string name, decimal salary)

{

this.id = id;

this.name = name;

this.salary = salary;

}

public override string ToString()

{

StringBuilder sb = new StringBuilder(100);

sb.AppendFormat("{0}: {1, -20} {2:C}", id.ToString(), name, salary);

return sb.ToString();

}

}

在示例程序的Main()方法中,创建一个新的Dictionary<TKey, TValue>实例,其中键是EmployeeId类型,值是Employee类型。构造函数指定了31个元素的容量。注意容量一般是素数。但如果指定了一个不是素数的值,也不需要担心。Dictionary<TKey, TValue>类会使用传送给构造函数的整数后面的一个素数,来指定容量。用Add()方法创建员工对象和ID,并添加到字典中。除了Add()方法外,还可以使用索引器,将键和值添加到字典中,如员工Jeff和Casey所示:

static void Main()

{

Dictionary<EmployeeId, Employee> employees =

new Dictionary<EmployeeId, Employee>(31);

EmployeeId idJimmie = new EmployeeId("B4711");

Employee jimmie = new Employee(idJimmie, "Jimmie Johnson", 8909140.00m);

employees.Add(idJimmie, jimmie);

Console.WriteLine(jimmie);

EmployeeId idTony = new EmployeeId("B12836");

Employee tony = new Employee(idTony, "Tony Stewart", 7285280.00m);

employees.Add(idTony, tony);

Console.WriteLine(tony);

EmployeeId idMatt = new EmployeeId("N34434");

Employee matt = new Employee(idMatt, "Matt Kenseth", 6608920.00m);

employees.Add(idMatt, matt);

Console.WriteLine(matt);

EmployeeId idJeff = new EmployeeId("J127");

Employee jeff = new Employee(idJeff, "Jeff Gordon", 5975870.00m);

employees[idJeff] = jeff;

Console.WriteLine(jeff);

EmployeeId idCasey = new EmployeeId("K100223");

Employee casey = new Employee(idCasey, "Casey Mears", 5413340.00m);

employees[idCasey] = casey;

Console.WriteLine(casey);

将数据项添加到字典中后,在while循环中读取字典中的员工。让用户输入一个员工号,把该号码存储在变量userInput中。用户输入X即可退出程序。如果输入的键在字典中,就使用Dictionary<TKey, TValue>类的TryGetValue()方法检查它。如果找到了该键,TryGetValue()方法就返回true,否则返回false。如果找到了与键关联的值,该值就存储在employee变量中,并写入控制台。

注意:

也可以使用Dictionary<TKey, TValue>类的索引器替代TryGetValue()方法,来访问存储在字典中的值。但是,如果没有找到键,索引器会抛出KeyNotFoundException类型的异常。

while (true)

{

Console.Write("Enter employee id (X to exit)> ");

string userInput = Console.ReadLine();

userInput = userInput.ToUpper();

if (userInput == "X") break;

EmployeeId id = new EmployeeId(userInput);

Employee employee;

if (!employees.TryGetValue(id, out employee))

{

Console.WriteLine("Employee with id {0} does not exist",

id);

}

else

{

Console.WriteLine(employee);

}

}

}

运行应用程序,得到如下输出:

Enter employee ID (format:A999999, X to exit)> J127

J000127: Jeff Gordon $5,975,870.00

Enter employee ID (format:A999999, X to exit)> N34434

N034434: Matt Kenseth $6,608,920.00

Enter employee ID (format:A999999, X to exit)> X

10.7.3 其他字典类

Dictionary<TKey, TValue>是Framework中的一个主要字典类,还有其他一些类,当然也有一些非泛型的字典类。

基于Object类型的字典和.NET 1.0以来可以使用的字典类如表10-8所示。

表 10-8

非泛型字典

说    明

Hashtable

Hashtable是.NET 1.0中最常用的字典类。键和值都基于Object类型

ListDictionary

ListDictionary位于命名空间System.Collections.Specialized,如果使用的元素少于10个,它就比Hashtable快。ListDictionary实现为链表

HybridDictionary

如果集合比较小,HybridDictionary就使用ListDictionary,当集合增大时,就改为使用Hashtable。如果事先不知道元素个数,就可以使用HybridDictionary

NameObjectCollectionBase

NameObjectCollectionBase是一个抽象基类,将字符串类型的键关联到Object类型的值上。它可以用作定制字符串/Object集合的基类这个类在内部使用Hashtable

(续表)  

非泛型字典

说    明

NameValueCollection

NameValueCollection派生于NameObjectCollection。它的键和值都是字符串类型。这个类有一个特性:多个值可以使用同一个键

Dictionary<TKey, TValue>

Dictionary<TKey, TValue>是一般用途的字典,将键映射到值上

SortedDictionary<TKey, TValue>

这是一个二叉搜索树,其中的元素根据键来排序。键的类型必须执行IComparable<TKey>接口。如果键的类型不能排序,还可以创建一个执行了IComparer<TKey>接口的比较器,将比较器用作有序字典的构造函数中的一个参数

SortedDictionary<TKey, TValue>和SortedList<TKey, TValue>的功能类似。但因为SortedList <TKey, TValue>实现为一个链表,而SortedDictionary<TKey, TValue>实现为一个字典,所以它们有不同的特性。

●       SortedList<TKey, TValue>使用的内存比SortedDictionary<TKey, TValue>少。

●       SortedDictionary<TKey, TValue>的元素插入和删除速度比较快。

●       在用已排好序的数据填充集合时,若不需要修改容量,SortedList<TKey, TValue>就比较快。

提示:

SortedList使用的内存比SortedDictionary少,但SortedDictionary在插入和删除未排序的数据时比较快。

10.8 带多个键的字典

在.NET Framework的字典中,每个键只支持一个值。不能为同一个键添加多个值。但是,可以创建基于Dictionary<TKey, TValue>的定制字典,将一个列表用作值。

类MultiDictionary<TKey, TValue>执行了接口IDictionary<TKey, TValue>。这个类包含了泛型类Dictionary,它的键TKey与外部的类相同,但其值定义为List<TValue>类型。每个键映射为一个List<TValue>类型的值。

using System;

using System.Collections.Generic;

namespace Wrox.ProCSharp.Collections

{

public class MultiDictionary<TKey, TValue> : IDictionary<TKey, TValue>

{

private Dictionary<TKey, List<TValue>> dict =

new Dictionary<TKey, List<TValue>>();

//...

 

MultiDictionary<TKey, TValue>要求实现接口IDictionary<TKey, TValue>的方法Add()、ContainsKey()、Remove()、TryGetValue(),属性Keys、Values,以及索引器。

Add()方法的实现代码首先用TryGetValue()确定键是否已添加到字典中。如果键已在字典中,就把新值添加到从TryGetValue()返回的列表中。否则,就创建一个新列表,再把值添加到这个新列表中,与列表相关的键添加到字典中。

public void Add(TKey key, TValue value)

{

List<TValue> list;

if (dict.TryGetValue(key, out list))

{

list.Add(value);

}

else

{

list = new List<TValue>();

newList.Add(value);

dict.Add(key, list);

}

}

//...

方法ContainsKey()、Remove()和属性Keys可以在向包含的字典发出请求时执行:

public bool ContainsKey(TKey key)

{

return dict.ContainsKey(key);

}

public ICollection<TKey> Keys

{

get

{

return dict.Keys;

}

}

public bool Remove(TKey key)

{

return dict.Remove(key);

}

//...

属性Values必须把List<TValue>类型的内部值转换为TValue类型。在完成while循环后,列表named values就包含所有列表中的所有值,这些列表在包含它们的字典中存储为值。while循环使用变量dict中的枚举器迭代所有的键,将所有的数据项添加到返回的列表中。

public ICollection<TValue> Values

{

get

{

List<TValue> values = new List<TValue>();

Dictionary<TKey, List<TValue>>.Enumerator enumerator = dict.
             GetEnumerator();

while (enumerator.MoveNext())

{

values.AddRange(enumerator.Current.Value);

}

return values;

}

}

//...

MultiDictionary不支持索引器和TryGetValue()方法,因为一个键可以关联多个值,所以不能返回一个值:

bool IDictionary<TKey, TValue>.TryGetValue(TKey key, out TValue value)

{

throw new NotSupportedException("TryGetValue is not supported");

}

TValue IDictionary<TKey, TValue>.this[TKey key]

{

get

{

throw new NotSupportedException(

"accessing elements by key is not supported");

}

set

{

throw new NotSupportedException(

"accessing elements by key is not supported");

}

}

//...

在这个类中,不能使用TValue类型的索引器,但可以使用List<TValue>类型的索引器:

public IList<TValue> this[TKey key]

{

get

{

return dict[key];

}

}

//...

接口IDictionary<TKey, TValue>派生于ICollection<KeyValuePair<TKey, TValue>>,所以还必须实现方法Add()、Clear()、Contains()、CopyTo()、Remove(),属性Count、IsReadOnly。

变元类型为KeyValuePair<TKey, TValue>的方法Add()只需调用Add()方法,其中的TKey和 TValue要分别传送。Clear()方法在包含元素的字典上调用Clear()。如果指定的键不存在,Contains()就返回false,否则,list.Contains()就确定值是否在列表中,并返回结果。CopyTo()把所有的数据项复制到一个数组中。属性Count返回所有值的个数。

public void Add(KeyValuePair<TKey, TValue> item)

{

Add(item.Key, item.Value);

}

public void Clear()

{

dict.Clear();

}

public bool Contains(KeyValuePair<TKey, TValue> item)

{

List<TValue> list;

if (!dict.TryGetValue(item.Key, out list))

{

return false;

}

else

{

return list.Contains(item.Value);

}

}

public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)

{

if (array == null)

throw new ArgumentNullException("array");

if (arrayIndex < 0 || arrayIndex > array.Length)

throw new ArgumentOutOfRangeException("array index out of range");

if (array.Length - arrayIndex < this.Count)

throw new ArgumentException("Array too small");

Dictionary<TKey, List<TValue>>.Enumerator enumerator = dict.GetEnumerator();

while (enumerator.MoveNext())

{

KeyValuePair<TKey, List<TValue>> mapPair = enumerator.Current;

foreach (TValue val in mapPair.Value)

{

array[arrayIndex++] = new KeyValuePair<TKey,TValue>(mapPair.Key, val);

}

}

}

public int Count

{

get

{

int count = 0;

Dictionary<TKey, List<TValue>>.Enumerator enumerator = dict.GetEnumerator();

while (enumerator.MoveNext())

{

KeyValuePair<TKey, List<TValue>> pair = enumerator.Current;

count += pair.Value.Count;

}

return count;

}

}

public bool IsReadOnly

{

get { return false; }

}

public bool Remove(KeyValuePair<TKey, TValue> item)

{

List<TValue> list;

if (dict.TryGetValue(item.Key, out list))

{

return list.Remove(item.Value);

}

else

{

return false;

}

}

//...

接口IDictionary<TKey, TValue>也派生于IEnumerable<KeyValuePair<TKey, TValue>>,所以还必须实现GetEnumerator()方法。这里可以使用yield return语句。

注意:

yield return语句详见第5章。

public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()

{

Dictionary<TKey, List<TValue>>.Enumerator enumerateKeys = dict.GetEnumerator();

while (enumerateKeys.MoveNext())

{

foreach (TValue val in enumerateKeys.Current.Value)

{

KeyValuePair<TKey, TValue> pair = new KeyValuePair<TKey, TValue>(

enumerateKeys.Current.Key, val);

yield return pair;

}

}

}

System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()

{

return GetEnumerator();

}

}

}

MultiDictionary<TKey, TValue>可以像一般的字典那样创建,但可以多次使用同一个键。在这种情况下,可以将国家用作键,将Racer用作值。将两个澳大利亚赛手和两个英国赛手添加到字典中。这个字典的索引器返回一个用foreach循环迭代的列表。

MultiDictionary<string, Racer> racers = new MultiDictionary<string, Racer>();

racers.Add("Canada", new Racer("Jacqes", "Villeneuve", "Canada", 11));

racers.Add("Australia", new Racer("Alan", "Jones", "Australia", 12));

racers.Add("United Kingdom", new Racer("Jackie", "Stewart",

"United Kingdom", 27));

racers.Add("United Kingdom", new Racer("James", "Hunt",

"United Kingdom", 10));

racers.Add("Australia", new Racer("Jack", "Brabham", "Australia", 14));

foreach (Racer r in racers["Australia"])

{

Console.WriteLine(r);

}

输出显示了澳大利亚赛手:

Alan Jones

Jack Brabham

10.9 位数组

如果需要处理许多位,就可以使用类BitArray和结构BitVector32。BitArray位于命名空间System.Collections,BitVector32位于命名空间System.Collections.Specialized。这两种类型最重要的区别是,BitArray可以重新设置大小,如果事先不知道需要的位数,就可以使用BitArray,它可以包含非常多的位。BitVector32是基于栈的,因此比较快。BitVector32仅包含32位,存储在一个整数中。

10.9.1 BitArray

类BitArray是一个引用类型,包含一个int数组,每32位使用一个新整数。这个类的成员如表10-9所示。

表 10-9

BitArray类的成员

说    明

Count, Length

Count和Length的get访问器返回数组中的位数。使用Length属性还可以定义新的数组大小,重新设置集合的大小

(续表)  

BitArray类的成员

说    明

Item

可以使用索引器读写数组中的位。索引器是bool类型

Get(), Set()

除了使用索引器之外,还可以使用Get和Set方法访问数组中的位

SetAll()

根据传送给该方法的参数,设置所有位的值

Not()

倒转数组中所有位的值

And(), Or(), Xor()

使用And()、Or和Xor()方法,可以合并两个BitArray对象。And()方法执行二进制的AND,只有两个输入数组的位都设置为1,结果位才是1。Or()方法执行二进制的OR,只要有一个输入数组的位设置为1,结果位就是1。Xor()方法是异或操作,只有一个输入数组的位设置为1,结果位才是1

注意:

第6章介绍了处理位的C#运算符。

帮助方法DisplayBits()迭代BitArray,根据位的设置情况,在控制台上显示1或0:

static void DisplayBits(BitArray bits)

{

foreach (bool bit in bits)

{

Console.Write(bit ? 1 : 0);

}

}

演示BitArray类的示例创建了一个包含8位的数组,其索引是0~7。SetAll()方法把这8位都设置为true。接着Set()方法把1位设置为false。除了Set()方法之外,还可以使用索引器,例如下面的索引5和7:

BitArray bits1 = new BitArray(8);

bits1.SetAll(true);

bits1.Set(1, false);

bits1[5] = false;

bits1[7] = false;

Console.Write("initialized:");

DisplayBits(bits1);

Console.WriteLine();

这是初始化位的显示结果:

initialized: 10111010

Not()方法会倒转BitArray的位:

DisplayBits(bits1);

bits1.Not();

Console.Write(" not ");

DisplayBits(bits1);

Console.WriteLine();

Not()方法的结果是所有的位全部倒转过来。如果某位是true,执行Not()方法的结果就是false,反之亦然。

10111010 not 01000101

这里创建了一个新的BitArray。在构造函数中,使用变量bits1初始化数组,所以新数组与旧数组有相同的值。接着把位0、1和4的值设置为不同的值。在使用Or()方法之前,显示位数组bits1和bits2。Or()方法将改变bits1的值:

BitArray bits2 = new BitArray(bits1);

bits2[0] = true;

bits2[1] = false;

bits2[4] = true;

DisplayBits(bits1);

Console.Write(" or ");

DisplayBits(bits2);

Console.Write(" = ");

bits1.Or(bits2);

DisplayBits(bits1);

Console.WriteLine();

使用Or()方法时,从两个输入数组中提取设置位。结果是,如果某位在第一个或第二个数组中设置为true,该位在执行Or()方法后就是true:

01000101 or 10001101 = 11001101

下面使用And()方法处理bits1和bits2:

DisplayBits(bits2);

Console.Write(" and ");

DisplayBits(bits1);

Console.Write(" = ");

bits2.And(bits1);

DisplayBits(bits2);

Console.WriteLine();

And()方法只把在两个输入数组中都设置为true的位设置为true:

10001101 and 11001101 = 10001101

最后使用Xor()方法进行异或操作:

DisplayBits(bits1);

Console.Write(" xor ");

DisplayBits(bits2);

bits1.Xor(bits2);

Console.Write(" = ");

DisplayBits(bits1);

Console.WriteLine();

使用Xor()方法,只有一个(不能是两个)输入数组的位设置为1,结果位才是1。

11001101 xor 10001101 = 01000000

10.9.2 BitVector32

如果事先知道需要的位数,就可以使用BitVector32结构替代BitArray。BitVector32效率较高,因为它是一个值类型,在整数栈上存储位。一个整数可以存储32位。如果需要更多的位,就可以使用多个BitVector32值或BitArray。BitArray可以根据需要增大,但BitVector32不能。

表10-10列出了BitVector32中与BitArray完全不同的成员。

表 10-10

BitVector32的成员

说    明

Data

属性Data把BitVector32中的数据返回为整数

Item

BitVector32的值可以使用索引器设置。索引器是重载的—— 可以使用掩码或BitVector32. Section类型的片断来确定和设置值。

CreateMask()

这是一个静态方法,用于为访问BitVector32中的特定位创建掩码

CreateSection()

这是一个静态方法,用于创建32位中的几个片断

 

示例代码用默认构造函数创建了一个BitVector32,其中所有的32位都初始化为false。接着创建掩码,以访问位矢量中的位。对CreateMask()的第一个调用创建了一个访问第一位的掩码。接着调用CreateMask(),将bit1设置为1。再次调用CreateMask(),把第一个掩码传送为参数,返回一个访问第二位(它是2)的掩码。接着,将bit3设置为4,以访问位号3。bit4的值是8,以访问位号4。

然后,使用掩码和索引器访问位矢量中的位,并设置字段:

BitVector32 bits1 = new BitVector32();

int bit1 = BitVector32.CreateMask();

int bit2 = BitVector32.CreateMask(bit1);

int bit3 = BitVector32.CreateMask(bit2);

int bit4 = BitVector32.CreateMask(bit3);

int bit5 = BitVector32.CreateMask(bit4);

bits1[bit1] = true;

bits1[bit2] = false;

bits1[bit3] = true;

bits1[bit4] = true;

bits1[bit5] = true;

Console.WriteLine(bits1);

BitVector32有一个重写的ToString()方法,它不仅显示类名,还显示1或0,来说明位是否设置了,如下所示:

BitVector32{00000000000000000000000000011101}

除了用CreateMask()方法创建掩码之外,还可以自己定义掩码,也可以一次设置多个位。十六进制值abcdef与二进制值1010 1011 1100 1101 1110 1111相同。用这个值定义的所有位都设置了:

bits1[0xabcdef] = true;

Console.WriteLine(bits1);

在输出中可以验证设置的位:

BitVector32{00000000101010111100110111101111}

把32位分别放在不同的片断中,是非常有用的。例如,IPv4地址定义为一个4字节数,存储在一个整数中。可以定义4个片断,把这个整数拆分开。在多播IP消息中,使用了几个32位值。其中一个32位值放在这些片断中:16位表示源号,8位表示查询器的查询内部码,3位表示查询器的健壮变量,1位表示抑制标志,还有4个保留位。也可以定义自己的位含义,以节省内存。

下面的例子模拟接收到值0x79abcdef,把这个值传送给BitVector32的构造函数,并设置位:

int received = 0x79abcdef;

BitVector32 bits2 = new BitVector32(received);

Console.WriteLine(bits2);

在控制台上显示了初始化的位:

BitVector32{01111001101010111100110111101111}

接着创建6个片断。第一个片断需要12位,由十六进制值0xfff定义(设置了12位)。片断B需要8位,C片断需要4位,D和E片断需要3位,F片断需要2位。第一次调用CreateSection(),只是接收0xfff,为最前面的12位分配内存。第二次调用CreateSection()时,将第一个片断传送为变元,使下一个片断从第一个片断的结尾处开始。CreateSection()返回一个BitVector32. Section类型的值,它包含了该片断的偏移量和掩码。

// sections: FF EEE DDD CCCC BBBBBBBB AAAAAAAAAAAA

BitVector32.Section sectionA = BitVector32.CreateSection(0xfff);

BitVector32.Section sectionB = BitVector32.CreateSection(0xff, sectionA);

BitVector32.Section sectionC = BitVector32.CreateSection(0xf, sectionB);

BitVector32.Section sectionD = BitVector32.CreateSection(0x7, sectionC);

BitVector32.Section sectionE = BitVector32.CreateSection(0x7, sectionD);

BitVector32.Section sectionF = BitVector32.CreateSection(0x3, sectionE);

把一个BitVector32. Section传送给BitVector32的索引器,会返回一个int,它映射到位矢量的片断上。这里使用一个帮助方法IntToBinaryString(),获得int数的字符串表示:

Console.WriteLine("Section A: " + IntToBinaryString(bits2[sectionA], true));

Console.WriteLine("Section B: " + IntToBinaryString(bits2[sectionB], true));

Console.WriteLine("Section C: " + IntToBinaryString(bits2[sectionC], true));

Console.WriteLine("Section D: " + IntToBinaryString(bits2[sectionD], true));

Console.WriteLine("Section E: " + IntToBinaryString(bits2[sectionE], true));

Console.WriteLine("Section F: " + IntToBinaryString(bits2[sectionF], true));

IntoToBinaryString接收整数中的位,返回一个包含0和1的字符串表示。在实现代码中迭代整数的32位。在迭代过程中,如果位设置为1,就在StringBuilder的后面追加1,否则,就追加0。在循环中,移动一位,以检查是否设置了下一位。

static string IntToBinaryString(int bits, bool removeTrailingZero)

{

StringBuilder sb = new StringBuilder(32);

for (int i = 0; i < 32; i++)

{

if ((bits & 0x80000000) != 0)

{

sb.Append("1");

}

else

{

sb.Append("0");

}

bits = bits << 1;

}

string s = sb.ToString();

if (removeTrailingZero)

{

return s.TrimStart('0');

}

else

{

return s;

}

}

结果显示了片断A~F的位表示,现在可以用传送给位矢量的值来验证了:

Section A: 110111101111

Section B: 10111100

Section C: 1010

Section D: 1

Section E: 111

Section F: 1

10.10 性能

许多集合类都提供了相同的功能,例如,SortedList与SortedDictionary的功能几乎完全相同。但是,其性能常常有很大区别。一个集合使用的内存少,另一个集合的元素检索速度快。在MSDN文档中,集合的方法常常有性能提示,给出了以大O记号表示的操作时间:

O(1)

O(log n)

O(n)

O(1)表示无论集合中有多少数据项,这个操作需要的时间都不变。例如,ArrayList的Add()方法就是O(1)。无论列表中有多少个元素,在列表尾部添加一个新元素的时间都是相同的。Count属性会给出元素个数,所以很容易找到列表尾部。

O(n)表示对于集合中的每个元素,需要增加的时间量都是相同的。如果需要重新给集合分配内存,ArrayList的Add()方法就是O(n)。改变容量,需要复制列表,复制的时间随元素的增加而线性增加。

O(log n)表示操作需要的时间随集合中元素的增加而增加,但每个元素需要增加的时间不是线性的,而是呈对数曲线。在集合中执行插入操作时,SortedDictionary<TKey,TValue>就是O(log n),而SortedList<TKey,TValue>是O(n)。这里SortedDictionary<TKey,TValue>要快得多,因为它在树形结构中插入元素的效率比列表高得多。

10.11 小结

本章介绍了如何处理不同类型的集合。数组的大小是固定的,但可以使用列表作为动态增长的集合。队列以先进先出的方式访问元素,栈以后进先出的方式访问元素。链表可以快速插入和删除元素,但搜索操作比较慢。通过键和值可以使用字典,它的搜索和插入操作比较快。

本章还介绍了许多接口及其在集合访问和排序上的用法。探讨了一些特殊的集合,例如BitArray和BitVector32,它们为处理位集合进行了优化。

 

posted @ 2008-08-24 19:29  ike_li  阅读(658)  评论(0)    收藏  举报