集合指南

注释

此内容由 Pearson Education, Inc. 的许可从 框架设计指南:可重用 .NET 库的约定、习惯和模式(第 2 版)重新打印。 该版于2008年出版,此后该书已于 第三版全面修订。 此页上的一些信息可能已过期。

任何专门设计用于操控具有某些共同特征对象组的类型都可以被视为集合。 几乎总是适合实现IEnumerableIEnumerable<T>的类型,因此在本部分中,我们仅将实现其中一个或两个接口的类型视为集合。

❌ 请勿在公共 API 中使用弱类型集合。

表示集合项的所有返回值和参数的类型应该是确切的项类型,而不是其任何基类型(这仅适用于集合的公共成员)。

❌ 请勿在公共 API 中使用 ArrayListList<T> 使用。

这些类型是设计用于内部实现而不是公共 API 的数据结构。 List<T> 在性能和功耗方面进行了优化,以牺牲 API 的简洁性和灵活性为代价。 例如,如果返回 List<T>,则当客户端代码修改集合时,将无法接收通知。 此外,List<T> 公开许多成员,例如 BinarySearch,在许多场景中没有用或不适合。 以下两节介绍了专为在公共 API 中使用的类型(抽象)。

❌ 请勿在公共 API 中使用 HashtableDictionary<TKey,TValue> 使用。

这些类型是设计用于内部实现的数据结构。 公共 API 应使用 IDictionaryIDictionary <TKey, TValue> 或自定义类型来实现一个或两个接口。

❌ 禁止使用 IEnumerator<T>IEnumerator或任何其他实现这些接口的类型,除非作为 GetEnumerator 方法的返回类型使用。

从非 GetEnumerator 方法返回枚举器的类型不能与 foreach 语句一起使用。

❌ 请勿在同一类型上同时实现 IEnumerator<T>IEnumerable<T>。 这同样适用于非泛型接口 IEnumeratorIEnumerable

集合参数

✔️ 请尽可能使用最不专用的类型作为参数类型。 将集合作为参数的大多数成员都使用该 IEnumerable<T> 接口。

❌ 避免只使用 ICollection<T>ICollection 用作参数来访问 Count 属性。

相反,请考虑使用 IEnumerable<T>IEnumerable,并动态检查对象是否实现 ICollection<T>ICollection

集合属性和返回值

❌ 请勿提供可设置的集合属性。

用户可以通过先清除集合,然后添加新内容来替换集合的内容。 如果替换整个集合是一种常见方案,请考虑在集合上提供 AddRange 方法。

✔️ 请对表示读/写集合的属性或返回值使用 Collection<T>Collection<T> 的子类。

如果Collection<T>不符合某些要求(例如集合不得实现IList),则通过实现IEnumerable<T>或使用ICollection<T>IList<T>自定义集合。

✔️ DO 使用 ReadOnlyCollection<T>、子类 ReadOnlyCollection<T>或极少数情况下 IEnumerable<T> 用于表示只读集合的属性或返回值。

一般情况下,首选 ReadOnlyCollection<T>。 如果它不满足某些要求(例如集合不得实现IList),则通过实现IEnumerable<T>或使用ICollection<T>IList<T>自定义集合。 如果确实实现了自定义只读集合,则实现 ICollection<T>.IsReadOnly 以返回 true

如果确定唯一希望支持的方案是仅向前迭代,则只需使用 IEnumerable<T>

✔️ 请考虑使用泛型基集合的子类,而不是直接使用集合。

这允许使用更有意义的名称,并添加在基础集合类型中不存在的辅助成员。 这特别适用于高级 API。

✔️ 考虑从非常常用的方法和属性返回 Collection<T> 的子类或 ReadOnlyCollection<T> 的子类。

这样,就可以在将来添加帮助程序方法或更改集合实现。

✔️ 如果集合中存储的项具有唯一键(名称、ID 等),请考虑使用键化集合。 键式集合是可以通过整数和键进行索引的集合,通常通过继承自 KeyedCollection<TKey,TItem> 实现。

键式集合通常具有更大的内存占用空间,如果内存开销超过拥有密钥的好处,则不应使用。

❌ 请勿从集合属性或返回集合的方法中返回 null 值。 改为返回空集合或空数组。

一般规则是应将 null 和空(0 项)集合或数组视为相同。

快照与动态集合

表示某个时间点的状态的集合称为快照集合。 例如,包含从数据库查询返回的行的集合将是快照。 始终表示当前状态的集合称为实时集合。 例如,项集合 ComboBox 是实时集合。

❌ 请勿从属性返回快照集合。 属性应返回实时集合。

属性 "getter" 的操作应该是非常轻量级的。 返回快照需要在 O(n)作中创建内部集合的副本。

✔️ 请使用快照集合或实时 IEnumerable<T> (或其子类型)来表示可变集合(即,无需显式修改集合即可更改)。

通常,表示共享资源(例如目录中的文件)的所有集合都是可变的。 除非实现只是一个仅向前遍历的枚举器,否则此类集合难以或不可能被实现为实时集合。

在数组和集合之间进行选择

✔️ 确实更喜欢集合而不是数组。

集合可以更好地控制内容、随时间推移而发展,并且更易于使用。 此外,不建议将数组用于只读方案,因为克隆数组的成本是令人望而却步的。 可用性研究表明,一些开发人员对使用基于集合的 API 感到更舒服。

但是,如果要开发底层 API,则在读写场景中使用数组可能更好。 数组的内存占用量较小,这有助于减少工作集,并且对数组中的元素的访问速度更快,因为它由运行时进行优化。

✔️ 请考虑在低级别 API 中使用数组,以最大程度地减少内存消耗并最大程度地提高性能。

✔️ 请使用字节数组而不是字节集合。

❌ 请勿对属性使用数组,如果每次调用属性的 getter 都必须返回一个新数组(例如内部数组的副本)。

实现自定义集合

✔️ 在设计新集合时,考虑继承自Collection<T>ReadOnlyCollection<T>KeyedCollection<TKey,TItem>

✔️ 在设计新集合时务必实施 IEnumerable<T> 。 请考虑在有意义的位置实现 ICollection<T> 或者甚至在有意义的位置实现 IList<T>

实现此类自定义集合时,请尽可能紧密地遵循由Collection<T>ReadOnlyCollection<T>建立的 API 模式。 也就是说,要显式实现相同的成员,将参数命名为类似这两个集合成员的名称,等等。

✔️ 考虑实现非泛型集合接口(IList 以及 ICollection)如果集合经常传递给将这些接口作为输入的 API。

❌ 避免在与集合概念无关的复杂 API 的类型上实现集合接口。

❌ 请勿继承自非泛型基集合,例如 CollectionBase。 请改用 Collection<T>ReadOnlyCollection<T>KeyedCollection<TKey,TItem>

命名自定义集合

集合(实现 IEnumerable的类型)主要由两个原因创建:(1)创建具有结构特定作的新数据结构,并且性能特征通常不同于现有数据结构(例如, List<T>LinkedList<T>Stack<T>和(2)的专用集合,用于保存一组特定项目(例如, StringCollection)。 数据结构最常用于应用程序和库的内部实现中。 专用集合主要在 API 中公开(作为属性和参数类型)。

✔️ 在实现 IDictionaryIDictionary<TKey,TValue> 的抽象名称中使用“Dictionary”后缀。

✔️ 确保在实现 IEnumerable(或其任何子类型)的类型名称中使用“Collection”后缀,并且这些类型应表示项列表。

✔️ 请对自定义数据结构使用适当的数据结构名称。

❌ 避免使用任何后缀表示特定实现,例如集合抽象名称中的“LinkedList”或“Hashtable”。

✔️ 请考虑使用项类型名称为集合名称添加前缀。 例如,存储类型 Address (实现 IEnumerable<Address>)的项的集合应命名 AddressCollection。 如果项类型是接口,则可以省略项类型的“I”前缀。 因此,可以将IDisposable项的集合称为DisposableCollection

✔️ 如果框架中可能添加或已存在相应的可写集合,请考虑在只读集合的名称中使用“ReadOnly”前缀。

例如,字符串的只读集合应称为 ReadOnlyStringCollection

部分内容 © 2005, 2009 Microsoft 公司。 保留所有权利。

经皮尔逊教育有限公司许可,从由 Krzysztof Cwalina 和 Brad Abrams 撰写的《框架设计准则:可重用 .NET 库的约定、习惯和模式》一书中重新印刷。此书由 Addison-Wesley Professional 于 2008 年 10 月 22 日出版,是微软 Windows 开发系列的一部分。

另请参阅