集合概述

在编程中,所谓的“集合”其最简单的形式是数组。集合正象它的意思所表达的那样,是由对象组成的组(或集合)。不同类型的集合将有不同的存储和访问方法 — 各种情况下的不同性能特征。

例如,一个数组在内存中是连续存储的。这意味着,对其编制索引(访问第 i 个元素)是很快的。但在数组中间插入元素却很慢,原因是要移动数组中插入元素后面的所有元素。(最坏的情况是,数组中没有空间,必须新建一个空间,然后将所有元素都复制到其中。)如果数组的内容是排序了的,在数组中进行搜索就可以很快(可以使用对分搜索),但如果没有排序,搜索就会很慢(因为必须依次查看每个元素)。

散列表中存储对值的引用,每个引用都与一个键相关。从逻辑观点看,可以将散列表中的键看作数组的下标:在向散列表提供键后,就会返回与该键相关的对象。(很明显,键必须是唯一的,就象数组下标一样。)实际上,许多语言,包括 C# 在内,都使用数组下标语法进行查找,比如 hashTab["key value"]。键可以是任何可以正确实现 Object.GetHashCodeObject.Equals 的类型,而不仅是 StringInt32

值不是连续存储的,不能用一个整数索引值在散列表中进行搜索(除非键是连续的整数)。但是,可以使用 foreach 枚举所有值。添加和删除元素都很快,因为是根据键进行元素查找的。

排序的列表类型将键数组和对对象引用的数组结合起来,提供了一个已排序数组,这样就可以按键索引和访问。它不象散列表那样快,但它在既需要索引又需要进行查找时却不失为好的选择。

可以组合多个集合,以便实现所需的性能特征。例如,可以使用指向列表元素或数组元素的散列表,以便在列表或数组中快速进行键查找。但必须确保散列表与列表或数组同步。最好是编写一个包含并能正确维护这两种容器的集合。

如果希望按照添加到集合时的顺序访问项目,请使用队列,队列是按先进先出的访问顺序进行优化的。如果希望按相反的顺序访问项目(后进先出),请使用堆栈。

许多系统还有链接的列表类型,但 .NET 框架没有。在经常需要添加和删除项目的情况下,链接的列表很有用,因为一旦找到正确的位置,就可以通过切换几个指针来添加项目。按顺序显示项目是很快的,但编制索引和进行搜索却很慢。在大多数情况下,一个基于数组的列表类型与链接的列表效果相同或更好一些, 但有时会经常需要在列表中添加或删除项目,这时链接的列表可能更适合。

公用的树结构在 .NET 框架也未包括。在大多数操作(添加、删除和搜索)中,它都是相当快的,但却无法有效地对其编制索引。不过,可以按顺序有效地穿越树的结构。大多数情况下,散列表或排序列表的效果都与树一样或更好一些,但散列表不能保持项目的顺序,排序列表在添加和删除项目时要慢一些。

.NET 框架集合:基本接口

为了解 .NET 框架集合,先了解一些几乎所有集合都实现的接口是很有用的。

IEnumerable

所有集合都实现至少两种接口:ICollectionIEnumerable

IEnumerable 只有一种方法,称为 GetEnumerator。它返回一个计数器对象,该对象实现 IEnumerator 接口。

C# 和 Visual Basic.NET 中的 foreach 语句依赖于实现 IEnumerable 的集合。

IEnumerator

IEnumerator 接口允许您枚举集合,换言之,即访问每个元素。如果集合是有序集合(如数组或列表),该访问将是按顺序进行的;而在无序的集合(如散列表)中,访问没有特定的顺序。IEnumerator 有一个只读属性,称为 Current,该属性返回一个对当前对象的引用。它还有两个方法:MoveNext,用于移至集合中的下一个对象,设置 Current 指向该对象,并且如果没有越出结尾就返回 True;Reset,用于在集合的开头部分之前将 Current 指针重新设置为不确定的值。创建计数器或调用 Reset 后首次访问 Current 之前,“必须”先调用 MoveNext,因为 Current 引用最初是不确定的。

请注意,集合并“不”自行实现 IEnumerator;相反,每次调用 GetEnumerator 时集合都返回一个独立的计数器对象。

下面有一个有关枚举集合的示例,既使用了 foreach,又直接使用了枚举接口。

ICollection

ICollection 是对 IEnumerable 的扩展,并新增三个只读属性和一个方法。三个属性为:Count,用于返回集合中的对象数;IsSynchronized,如果对于多线程访问,对集合的访问是同步的,该属性就返回 True;SyncRoot,该属性返回的对象可用于对跨线程的集合访问进行同步(如果还没有进行同步)。一个方法为:CopyTo,用于将集合中的元素复制到数组,并在指定的数组位置开始。

使用 ICollection 和 IEnumerable 的示例

每个集合都可以告诉您集合中含有多少元素,都可以被枚举和复制,并且能够被同步。因此,可以将“任何”集合传递给下列方法:

public static void PrintCollection(String info, ICollection c) {   Console.Write("{0}:集合有 {1} 个元素:", info, c.Count);   foreach (Object o in c) { // 编写循环的最简便方法      Console.Write("{0} ", o);   }   Console.WriteLine();}public static void PrintCollectionSync(ICollection c) {   Console.WriteLine("同步访问(此处不需要)");   lock (c.SyncRoot) {  // 锁定 SyncRoot   //--仅用于演示,仅在多线程访问时才需要   // 以下循环与上面的 foreach 等价      IEnumerator e = c.GetEnumerator();      while (e.MoveNext()) { // 首次移动         Console.Write("{0} ", e.Current);      }      Console.WriteLine();   }}public static Object [] CopyToArray(ICollection c) {   Object [] retVal = new Object[c.Count];   c.CopyTo(retVal, 0);   return retVal;}

请注意,只要编程语言支持,foreach 就是首选的访问集合的方法,C# 和 Visual Basic.NET 都具有这一特征。除非有合理的理由,否则就应该使用 foreach,因为使用它可以允许编译器生成更适合的代码供特定的集合访问使用。无论您是否使用同步(在上述的第二个方法中,我们这样做了,但除非多个线程可以同时访问同一集合,否则没有必要同步),都应该如此。

在下面的示例中,我们要传入两个不同的集合:一个 Int32 数组和一个 Object 数组。可以自行对其他集合试用这些方法调用:

Int32 [] intArr = {0, 1, 2, 3, 4};PrintCollection("intArr", intArr);Object [] oArray = CollectionExample.CopyToArray(intArr);PrintCollection("oArray(从 intArr 复制得到)", oArray);PrintCollectionSync(oArray);

IList

许多集合(特别是那些排了序因而可编制索引的集合)还实现 IListIList 针对可编制索引的有序列表扩展了 ICollection(可能您还记得,ICollection 是对 IEnumerable 的扩展)。IList 新增了一个属性(在 Beta 2 及更高版本中增加三个属性)和七个方法。

一个属性为 Item,用于获取索引并返回相应的列表元素。(Item 是 C# 的索引函数,因此对集合 Foo 而言,在 C# 中访问 Item 属性所用的语法是 Foo[indexer]。) 在 Beta 2 及更高版本中,还有两个属性:IsFixedSizeIsReadOnly。它们在列表大小固定或为只读的情况下分别返回 True。

Ilist 中有许多方法:Clear 可清除列表;Add 可在列表中添加项目,通常添加到结尾(取决于实际实现);Insert 可在列表中指定的索引处插入项目;Remove 可删除某一对象的第一个实例;RemoveAt 可删除指定索引处的对象;Contains 可表明列表是否包含某个值;IndexOf 返回列表中值所在位置的索引。请注意,在大小固定的集合(如数组)上不能使用 AddInsertRemoveRemoveAt 方法。如果这样做,就会导致异常错误。

但我们可以轻松地从数组创建 ArrayList — 我们可以通过它演示 IList 方法。若要创建 ArrayList 并进行调用,要编写的代码很简单:

ArrayList al = new ArrayList(intArr);ListTest(al);

ListTest 方法也相当简单:

public static void ListTest(IList l) {   PrintCollection("原列表", l);         l.Add(5);   PrintCollection("Add(5) 之后", l);         l.RemoveAt(2);   PrintCollection("RemoveAt(2) 之后", l);         l.Remove(3);   PrintCollection("Remove(3) 之后", l);         l.Insert(1, 7);   PrintCollection("Insert(1, 7) 之后", l);         Console.WriteLine("包含 55?{0}", l.Contains(55));   Console.WriteLine("IndexOf(4) 是 {0}", l.IndexOf(4));   l.Clear();   PrintCollection("清除之后", l);}

这种情况下,输出为:

原列表:集合有 5 个元素:0 1 2 3 4Add(5) 之后:集合有 6 个元素:0 1 2 3 4 5RemoveAt(2) 之后:集合有 5 个元素:0 1 3 4 5Remove(3) 之后:集合有 4 个元素:0 1 4 5Insert(1, 7) 之后:集合有 5 个元素:0 7 1 4 5包含 55?FalseIndexOf(4) 是 3清除之后:集合有 0 个元素:

.NET 框架集合

至此,我们已经看到了几个用于集合的接口,那么不妨再看一下在 .NET 框架中可用的集合。

数组

与某些系统不同,.NET 框架中的数组类型是一个很丰富的集合:它有许多可供处理数组的方法。首先,数组实现 IList,因此在将数组传递给获得 IList 的方法或者将数组转换到 IList 时可以将其视为列表。(不过请注意,数组是固定大小的,因此 AddInsertRemoveRemoveAt 不可用。)由于 IList 是对 ICollectionIEnumerable 的扩展,因此也可以使用这两个接口处理数组。数组实现这些接口意味着,可以将它们用于 Windows 窗体和 ASP.NET 中的数据绑定控件。

不过要注意,虽然可以用 Array.CreateInstance 创建任何支持类型的数组,但不能从数组中衍生新的类,也不能直接创建自己的数组类。“.NET 框架参考”中有一个使用 Array.CreateInstance 的示例(英文)

还记得上次讨论时,System.Array 实现大量处理数组的方法,包括对数组进行搜索、排序和转置操作。

ArrayList

ArrayList 类实现大小动态变化的数组,这样的数组非常类似于内置数组,唯一不同的是初始化稍微怪异一些,数组大小可以根据需要变大。还可以通过设置数组的 Capacity 属性明确地增大数组的大小,或者通过设置 Capacity 或调用 TrimToSize 将其缩小到仅包括当前数量的元素。容量是在不扩大 ArrayList 的情况下可以容纳的元素数,与 ArrayList 中当前具有的元素数不同。Capacity 总是大于或等于 Count

可以使用现有的 ICollection、用于指定容量的 Int32,或者根本不使用参数来构造 ArrayList。还可以通过使用 Repeat 静态方法,创建使用某一重复值进行初始化的 ArrayList

ArrayList 的功能在其他方面与数组和其他实现 IList 的类相似。不过,有些方法,如 BinarySearchSortReverse,在 ArrayList 中是实例方法,但在 Array 中它们是静态方法,因此 ArrayArrayList 的编码略微不同。

ArrayList 类还有一些方法,可处理同时对整个集合进行的添加/插入/删除/替换/检索操作。这些方法都以“Range”结尾:AddRange 获得一个集合并将其添加到 ArrayList 的结尾;GetRangeArrayList 的部分复制到新的 ArrayList 中;SetRange 将作为其参数传递的集合复制到 ArrayList 中的现有元素;InsertRange 将作为其参数传递的集合插入 ArrayList 中的指定位置;RemoveRangeArrayList 中删除一组元素;ToArray 返回新的数组,该数组由指定的对象或类型组成,包含 ArrayList 的内容。

最后,有一些静态方法可为现有的数据结构提供特定目的的包装:Adapter 返回引用 Ilist 中项目的 ArrayList,如果希望使用 ArrayList 方法对与 IList 兼容的集合进行排序、转置或搜索操作,这一包装就很方便;FixedSize 为大小无法更改的 ArrayList 返回包装;ReadOnly 为无法更改的 ArrayList 返回包装;Synchronize 为线程安全的 ArrayList 返回包装。

请注意,如果不需要 ArrayList 的特殊功能,就应考虑使用常规数组,从而略微提高性能 — 运行时对数组提供了一些内置优化,而对其他类型则没有。

Hashtable

.NET 框架中主要的非数组集合是 Hashtable 类。在散列表中存储着成对的键和对象引用。若要检索一个对象,只要给散列表提供一个键,它就会返回对该键所对应的对象的引用。对于添加和删除对象/键对,散列表速度很快。但由于它们是无序的,因此无法用整数对其编制索引(除非键是连续的整数,但这种情况下应使用数组),也无法将其排序或按顺序打印。可以对其进行枚举,但项目的顺序是不确定的。(请注意,可以让 ArrayList 引用的对象与 Hashtable 引用的对象相同,这样您就既可以编制索引/排序,也可以快速搜索数据结构 — 它在这两方面表现最好 — 只要同时更新这两种数据结构即可。)

散列表的工作方式是,将键和对象引用放入存储桶数组中。对于给定键而言,存储桶的索引通常是通过调用键对象的 GetHashCode 方法计算的。(是否记得对象讨论中的 GetHashCode? 现在到了使用它的时候。)多个键与同一个存储桶对应是可以的。在这种希望渺茫的情况下,要访问对象,就需要对整个存储桶的内容进行搜索。

如果要在散列表中用作键的对象没有可用的 GetHashCode 实现,或者如果需要对特定数据使用自定义散列函数,可以创建实现 IHashCodeProvider 接口(其中仅包含 GetHashCode 方法)的对象。然后,要创建这些对象中的一个,并将其传递给适当的 Hashtable 构造函数,这样在计算散列代码时就会调用您的方法。有一个内置的比较对象,称为 CaseInsensitiveHashCodeProvider.Default,需要时可以使用它。

散列表还需要将要查找的键与所存储的键进行比较。为此,它们要依赖于您是否正确实现 Object.Equals。这就是为什么如果覆盖 Object.Equals,也要覆盖 GetHashCode 的原因 — 这两个方法都是在散列表中进行键查找所必需的。

还可以通过创建实现 IComparer(其中包含一个方法:Compare)的对象,并在构造散列表时将其传递给适当的构造函数,从而对键比较进行自定义。有两个内置的比较函数可供使用:Comparer.DefaultCaseInsensitiveComparer.Default

Hashtable 实现的主要接口是 IDictionary,它是对 ICollection(及其基本 IEnumerable)的扩展。除基本接口方法的成员外,Hashtable 还实现 IDictionary 成员 — 在具有键和值的集合中使用的若干方法和属性。属性有 KeysValues,它们返回集合的键和值的 ICollections;还有 Item,键要传递给它,而它返回对相应值的引用。(这是 C# 的索引函数,使用数组下标可以访问它。)

IDictionary 方法有:Add(添加键/值对)、Remove(删除键/值对)、Clear(清除词典)、Contains(如果词典中含有特定的键,它就返回 True)和 GetEnumerator(返回 IDictionaryEnumerator 而不是 IEnumerator)。

IDictionaryEnumerator 是对 IEnumerator 的扩展,并新增三个属性:Entry,返回 DictionaryEntry 结构,其中包含对当前键及值的引用;Key,返回当前键;Value,返回对当前值的引用。(仍使用 MoveNext 移至集合中的下一个项目。)

IDictionary 中的 Contains 方法外,Hashtable 还提供 ContainsKey(与 Contains 相同)和 ContainsValue 方法。Contains/ContainsKey 进行散列表查找,速度很快。ContainsValue 对整个散列表进行线性搜索,因此速度很慢。

示例

虽然对集合进行了大量说明,但实际上它非常易用。我们将要使用 CaseInsensitiveHashtable(它是从 Hashtable 中衍生的),而不是普通的 Hashtable,这样即使将字符串用作键也不必进行匹配。请注意,CaseInsensitiveHashtable 在 system.dll 中,而不在 mscorlib.dll 中。我们必须在 csc 命令行添加 /r 开关,才可以进行编译:

csc /r:system.dll file.cs// IDictionary 集合的新重载public static void PrintCollection(String info, IDictionary c) {   Console.Write("{0}:集合有 {1} 个元素:", info, c.Count);   foreach (DictionaryEntry e in c) { // 编写循环的最简便方法      Console.Write("{0}/{1} ", e.Key, e.Value);   }   Console.WriteLine();}public static void HashTest() {   Hashtable h = new CaseInsensitiveHashtable(5);   String [] sList = {"Zero", "One", "Two", "Three", "Four"};   Int32 count = sList.Length;   for (Int32 i = 0; i < count; i++) {      h.Add(sList[i], i);   }   h.Add(5, "FIVE"); // 注意键和值类型不同   Console.WriteLine("顺序未定义");   PrintCollection("散列表", h);   Console.WriteLine("键‘fOuR’的值是 {0}", h["fOuR"]);      // 输出:键‘fOuR’的值是 4   Console.WriteLine("键 5 的值是 {0}", h[5]);      // 输出:键 5 的值是五}

请注意,可以使用散列表表示任何类型的对象对之间的相关性。键可以是任意类型(甚至可以在同一散列表中混合多种类型),值也可以是任意类型。唯一的要求是键类型必须正确实现 GetHashCodeEquals(或者您必须提供实现 IHashCodeProviderIComparer 的对象),键在被散列表引用时必须是不变的。如果键发生改变,就找不到它了。因此,最好使用不变类型的键。(请注意,String 是不变的。)

实现 GetHashCode

散列表实际上很易用,而且性能良好 — 如果散列函数完备,键就会均衡地分布到存储桶中。编写完备的散列代码生成器颇具艺术性,很难掌握。但要说明的是,如果没有完备的散列函数,则还可以使用其他集合(包括下述的一个集合)代替散列表。

SortedList/CaseInsensitiveSortedList

SortedList 类介于散列表和数组之间:可以使用对分搜索快速查找键(但不如散列表快)。此外,还可以对 SortedList 编制索引。换言之,SortedList 既实现 IDictionary 又实现 IList(而且具有二者的所有成员) 。从内部来说,SortedList 使用数组对进行存储:一个用于键,一个用于值。

有几种以前不曾见过的方法:GetKeyListGetValueListKeysValues 属性相似,唯一不同的是前者返回有序的、可编制索引的 IList,而不是 ICollectionGetByIndexSetByIndex 允许您按照索引而不是按照键访问值;IndexOfKeyIndexOfValue 返回某一键或值的索引 — 请注意,IndexOfValue 很慢,原因在于它是线性搜索。(不过,请注意,按照索引访问 SortedList 要比访问散列表快。)

SortedList 的代码与散列表相似:

public static void SortedListTest() {   SortedList sl = new CaseInsensitiveSortedList();   String [] s = {"Zero", "One", "Two", "Three", "Four"};   Int32 count = s.Length;   for (Int32 i = 0; i < count; i++) {      sl.Add(s[i], i);   }   Console.WriteLine("注意:按字符串排序,不是按值!");   PrintCollection("SortedList", sl);   // 输出:Four/4 One/1 Three/3 Two/2 Zero/0   Console.WriteLine("键‘fOuR’的值是 {0}", sl["fOuR"]);      // 输出:键‘fOuR’的值是 4   Console.WriteLine("索引 4 的键/值是 {0}/{1}",             sl.GetKey(4), sl.GetByIndex(4));      // 输出:索引 3 的键/值是零/0 }

Queue/Stack

QueueStack 类实现传统的 FIFO(先进先出)队列和 LIFO (后进先出)堆栈。二者都是使用数组在内部实现的。对二者而言,都要确保所用的数组足够长。如果在向集合中添加对象的过程中,数组长度不够,则会分配新的数组,并将原有数组复制到其中 — 成本很高的操作。

这两个集合都实现 ICollection(并因此实现 IEnumerable)。此外,它们还有一些有趣的方法。二者都有 Peek 方法,可允许您查看集合头中的对象而不删除它。将项目添加到堆栈中的方法称为 Push,添加到队列中的方法称为 Enqueue。删除堆栈头的方法称为 Pop,删除队列头的方法称为 Dequeue

下面是一个使用队列和堆栈的简单示例:

public static void QueueStackTest() {   Queue q = new Queue();   Stack s = new Stack();   for (Int32 i = 0; i < 5; i++) {      q.Enqueue(i);      s.Push(i);   }   Object o;   PrintCollection("Queue", q);   Console.Write("从队列删除:");   while (q.Count > 0) {      o = q.Dequeue();      Console.Write("{0} ", o);   }   // 输出:0 1 2 3 4   Console.WriteLine();   PrintCollection("Stack", s);   Console.Write("从堆栈删除:");   while (s.Count > 0) {      o = s.Pop();      Console.Write("{0} ", o);   }   // 输出:4 3 2 1 0   Console.WriteLine();}

BitArray

如果希望处理压缩的位数组,BitArray 类很适合。它允许您将 BooleanByteInt32 的数组一位一位地复制到可以处理的位字符串中。它还支持位的设置和清除,以及 AndOrXorNot 方法。

“.NET 框架参考”中有大量示例,因此 Dr. GUI 不在此重复了。

StringCollection

StringCollection 类是极为简单的、基于 IListString 集合。它与集合(如 ArrayList)相比,主要优点在于,它的类型很稳定 — 当您从集合中取得一个值时,它是 String,而不是 Object,而 Object 需要转换为 String。请到 .NET 框架中的查阅文档(英文),以获得详细信息。

其他

其余的集合大都处于 Beta 1 和 Beta 2 之间的过渡状态,因此我们现在跳过它们。可以通过以下方法查阅它们:在“.NET 框架参考”中查看 System.Collections 名称空间及其子名称空间 System.Collections.*。子名称空间的名称在 Beta 2 中会有更改。

编写自己的集合

如果想编写一个自己的集合,则可能的话,最好基于现有的集合创建 — 查看结尾为“Base”的类,也可以只是继承另一个集合或者包含另一个集合。

即使无法使用一个基类,也最好是实现适当的标准接口,如 IListICollectionIDictionaryIEnumerable。这样做可允许您的集合被其他 .NET 框架代码(例如,数据绑定控件)无缝地使用。

类型安全的集合和通用类

在使用 .NET 框架集合时您将会注意到,在从集合中获取项目时,要进行大量的类型转换,原因是它们在内部通常存储成 Object。可以不经转换而将它们放入,但若要取出它们,就需要在将它们指派给类型固定的引用时进行转换。

使用 Object 引用的事实还表明,放入集合中的值类型将被包装。如果正在创建包含值类型的集合,则更有效的做法是创建要使用的引用类型,以避免大量包装和解开包装操作。

问题在于,由于可以在集合中放入“任何”类型,因此很容易放入类型有误的对象。是否记得,当您进行类型转换时,运行时会检查类型 — 如果不能进行转换,就会出现异常错误。

如果可以有一个 Add 方法,只在集合中放入所需的类型,则集合的使用将更安全,也更容易,因为这样就不会在集合中放入任何“错误”类型。

C++ 程序员知道,高明的解决方案是:模板(也称为通用类或用参数表示的类型)。如果您不是 C++ 程序员,使用模板可以创建类或函数,其中某些参数实际上是类型,而不是变量。换言之,可以创建能处理任何通用(而不是类型安全)的类。

.NET 框架现在没有通用类,但以后的版本中会有。

如果确实需要类型安全,可以进行一些工作来实现。新建一个类并对其中适当的容器进行声明。然后,提供用户将使用的方法,让他们处理所包含的容器。下面是一个特别简化的示例:

class StringQueue {   private Queue q = new Queue();   public void Enqueue(String s) {      q.Enqueue(s);   }   public String Peek() {      return (String)q.Peek();   }   public String Dequeue() {      return (String)q.Dequeue();   }}// 测试StringQueue sq = new StringQueue();sq.Enqueue("Hello");// sq.Enqueue(1); // 不编译String s = sq.Dequeue(); // 不转换!Console.WriteLine("StingQueue: {0}", s);

问题在于,必须实现希望用户能访问的每个方法,而您无法实现允许添加错误类型的接口,因此这不是明智的解决方案。不过,Dr. GUI 已经听说 Beta 2 中将有一个工具,能够生成正确的类型安全的代码 — 这会很有用。

试用一下!

您知道沥青吗?如果不亲自用手试一试,把手弄脏,就不能了解这种材料。这次,要狠试一把了!

组织学习小组?

如果与别人合作,就会更有意思,您学到的东西也更多 — 从而发现与他人合作您可以进一步探求 .NET 的奥秘。

试试看…

下面这些看法可强化您的学习:

  • 如果还没有数据类型,请创建一个,如复数结构或员工记录类,并实现自定义的格式化程序,该格式化程序使用 formatstring 指定格式。将您的类放在 Console.WriteLineString.Format 中试一试。

  • 为内置类型编写自定义的格式化程序,并通过 String.Format 调用它。

  • 修改本文列举的代码示例,完成其他的事情,实现更重要的工作。

  • 对原有的使用数组的代码进行转换,以便使用适当的集合。

  • 使用散列表编写查找表。

  • 使用前面示例中的方法创建类型安全的集合。

  • 新建一个实现 IList 的集合(可能是一个链接列表)。通过多种方案将性能与内置 ArrayList 类进行比较 — 可能使用辅助的 HashTable

  • 高级:创建双重链接的列表。从 IEnumeratorIList 中衍生新的接口,这样就可以从前向后和从后向前遍历列表。通过多种方案将结果的性能与内置 ArrayList 类进行比较。