大部分计算机科学和编程的入门课本中都会有一章介绍集合。它们可能被称为数组或数据结构,但概念是相同的。将正式数据对象中的一组元素与另一组元素联系在一起的能力对现代编程技术来说是必需的。
在 Microsoft® .NET Framework 中,付出的很多努力都是为了创建强大的集合类,以满足各种需求及样式。这些集合使用方便、直观,并有足够的性能,这些都是非常重要的特征。在本月的这一期 内容中,我将着眼于 .NET 中的集合、它们的工作原理、使用它们的时机和一些最佳实践。

  • 各个元素具有相似的用途和同样的重要性。
  • 元素的数量未知,或者没有在编译时固定。
  • 需要支持对所有元素的遍历。
  • 需要支持对元素的排序。
  • 需要公开某个库的元素,并且预期用户在此使用集合类型。
注意,出于本栏目之目的,我认为数组 (System.Array) 将成为一种特殊类型的集合。

如果需要非常快地添加、删除和查找项目,而且不关心集合中项目的顺序,那么首先应该考虑使用 System.Collections.Generic.Dictionary<TKey, TValue>(或者您正在使用 .NET Framework 1.x,可以考虑 Hashtable)。三个基本操作(添加、删除和包含)都可快速操作,即使集合包含上百万的项目。另一方面,利用 List<T>(或 .NET Framework 1.x 中的 ArrayList)插入和删除项目所需的时间都可能有所不同。(List<T> 和 ArrayList 都在基础数组中存储项目,并保持顺序。添加项目可能需要移动基础数组中现有的项目以腾出空间。在末尾添加项目不需要进行任何移动,而且速度非常快)。
如果您的使用模式很少需要删除和大量添加,而重要的是保持集合的顺序,那么您仍然可以选择 List<T>。虽然查找速度可能比较慢(因为在搜索目标项目时需要遍历基础数组),但可以保证集合会保持特定的顺序。另外,您可以选择 Queue<T> 实现先进先出 (FIFO) 顺序或 Stack<T> 实现后进先出 (LIFO) 顺序。虽然 Queue<T> 和 Stack<T> 都支持枚举集合中的所有项目,但前者只支持在末尾插入和从开头删除,而后者只支持从开头插入和删除。
如果需要在实现快速插入的同时保持顺序,那么使用新的 LinkedList<T> 集合可帮助您提高性能。与 List<T> 不同,LinkedList<T> 是作为动态分配的对象链实现。与 List<T> 相比,在集合中间插入对象只需要更新两个连接和添加新项目。从性能的角度来看,链接列表的缺点是垃圾收集器会增加其活动,因为它必须遍历整个列表以确保没 有对象没有被释放。另外,由于每个节点相关的开销以及每个节点在内存中的位置等原因,大的链接列表可能会出现性能问题。虽然将项目插入到 LinkedList<T> 的实际操作比在 List<T> 中插入要快得多,但是找到要插入新值的特定位置仍需遍历列表并找到正确的位置。
如前所述,Dictionary<TKey, TValue> 可能最适用于快速插入和查找项目。但是,较快的查找速度只是针对普通情况而言,某些数据集可能导致性能急剧下降。 SortedDictionary<TKey,TValue> 是一个不同的实现,它用平衡树实现作为基础数据存储;这相对提高了查找速度并保持项目的排列顺序,但插入很有可能会慢一些(随集合中的项目数量不同而有所 差异)。或者也可以用 SortedList<Tkey,TValue>,它使用两个独立的数组分别保存键和值,并保持两者的顺序(最坏的情况是必须移动所有键和 值)。

例如,假设您需要实现一个简单的 System.Double 值集合,但还希望支持使用 Double.TryParse 的 Add(string) 方法,只在将值成功解析为 Double 后才添加结果。System.Collections.ObjectModel.Collection<T> 类实现了所有的标准集合接口,为了提供附加的 Add 重载,您的自定义集合类型从 Collection<Double> 派生即可。
class DoubleCollection : Collection<double>
{
    public void Add(string st)
    {
        Double d;
        if (Double.TryParse(st, out d)) base.Add(d);
        else throw new ArgumentException(
            “Cannot parse string to a double. “ +
            “Item was not added to collection.”);
    }
随着扩展方法的引入,.NET Framework 3.5 可以提供额外的机制来创建扩展现有类型的方法。例如,您可以在 C# 3.0 中重新编写此方法作为扩展方法,如下所示:
class CollectionExtensions
{
    public static void Add(this Collection<Double> c, string st)
    {
        Double d;
        if (Double.TryParse(st, out d)) c.Add(d);
        else throw new ArgumentException(
            “Cannot parse string to a double. “ +
            “Item was not added to collection.”);
    }
}
如果一个 Collection<Double> 有命名的值,则此静态方法可通过传统语法将新值添加到集合中。

 

CollectionExtensions.Add(values, “3.14”); 

 

但是,因为这是一个扩展方法(显然“this”关键字将第一个参数归为静态 Add 方法),您可以按如下所示重新编写:
values.Add(“3.14”);
如果用新类型扩展了 Collection<Double>,那么您会编写完全相同的方法调用,但您现在可以将此新 Add 方法与任何 Collection<Double> 实例(或其派生的任何类型)配合使用,而不只是与自定义类型一起使用。或者您可以开始构建自己的集合,不需要继承任何现有的集合。这种情况下,您可以利用 .NET Framework 中提供的适当接口。

此刻您应该对 .NET Framework 中的集合、它们是什么,以及使用它们的时机和原因有了很好的理解。集合可以是编程工具箱中的强大工具,因此了解它何时有用以及如何聪明地使用它都非常重要。如果您需要更多的信息,请参阅“更多参考资料”侧栏中的资源。

相关文章: