B.2 列表

  从很多方面来说,列表是最简单也最自然的集合类型。框架中包含很多实现,具有各种功能 和性能特征。一些常用的实现在哪里都可以使用,而一些较有难度的实现则有其专门的使用场景。

B.2.1  List<T>

  在大多数情况下, List<T> 都是列表的默认选择。它实现了 IList<T> ,因此也实现了 ICollection<T> 、 IEnumerable<T> 和 IEnumerable 。此外,它还实现了非泛型的 ICollection 和 IList 接口,并在必要时进行装箱和拆箱,以及进行执行时类型检查,以保证新元素始终与 T 兼容。

   List<T> 在内部保存了一个数组,它跟踪列表的逻辑大小和后台数组的大小。向列表中添加 元素,在简单情况下是设置数组的下一个值,或(如果数组已经满了)将现有内容复制到新的更 大的数组中,然后再设置值。这意味着该操作的复杂度为O(1)或O(n),取决于是否需要复制值。 扩展策略没有在文档中指出,因此也不能保证——但在实践中,该方法通常可以扩充为所需大小 的两倍。这使得向列表末尾附加项为O(1)平摊复杂度(amortized complexity);有时耗时更多,但 这种情况会随着列表的增加而越来越少。

  你可以通过获取和设置 Capacity 属性来显式管理后台数组的大小。 TrimExcess 方法可以 使容量等于当前的大小。实战中很少有必要这么做,但如果在创建时已经知道列表的实际大小, 则可将初始的容量传递给构造函数,从而避免不必要的复制。

   从 List<T> 中移除元素需要复制所有的后续元素,因此其复杂度为O(nk),其中k为移除元 素的索引。从列表尾部移除要比从头部移除廉价得多。另一方面,如果要通过值移除元素而不是 索引(通过 Remove 而不是 RemoveAt ),那么不管元素位置如何复杂度都为O(n):每个元素都将 得到平等的检查或打乱。

   List<T> 中的各种方法在一定程度上扮演着LINQ前身的角色。 ConvertAll 可进行列表投影; FindAll 对原始列表进行过滤,生成只包含匹配指定谓词的值的新列表。 Sort 使用类型默认的或 作为参数指定的相等比较器进行排序。但 Sort 与LINQ中的 OrderBy 有个显著的不同: Sort 修改原 始列表的内容,而不是生成一个排好序的副本。并且, Sort 是不稳定的,而 OrderBy 是稳定的; 使用 Sort 时,原始列表中相等元素的顺序可能会不同。LINQ不支持对 List<T> 进行二进制搜索: 如果列表已经按值正确排序了, BinarySearch 方法将比线性的 IndexOf 搜索效率更高 ① 。

  List<T> 中略有争议的部分是 ForEach 方法。顾名思义,它遍历一个列表,并对每个值都执行 某个委托(指定为方法的参数)。很多开发者要求将其作为 IEnumerable<T> 的扩展方法,但却一 直没能如愿;Eric Lippert在其博客中讲述了这样做会导致哲学麻烦的原因(参见http://mng.bz/Rur2)。 在我看来使用Lambda表达式调用 ForEach 有些矫枉过正。另一方面,如果你已经拥有一个要为列 表中每个元素都执行一遍的委托,那还不如使用 ForEach ,因为它已经存在了。① 二进制搜索的复杂度为O(log n),线性搜索为O(n)。

B.2.2 数组

  在某种程度上,数组是.NET中最低级的集合。所有数组都直接派生自 System.Array ,也是 唯一的CLR直接支持的集合。一维数组实现了 IList<T> (及其扩展的接口)和非泛型的 IList 、 ICollection 接口;矩形数组只支持非泛型接口。数组从元素角度来说是易变的,从大小角度 来说是固定的。它们显示实现了集合接口中所有的可变方法(如 Add 和 Remove ),并抛出 NotSupportedException 。

   引用类型的数组通常是协变的;如 Stream[] 引用可以隐式转换为 Object[] ,并且存在显式 的反向转换 ① 。这意味着将在执行时验证数组的改变——数组本身知道是什么类型,因此如果先 将 Stream[] 数组转换为 Object[] ,然后再试图向其存储一个非 Stream 的引用,则将抛出 ArrayTypeMismatchException 。

   CLR包含两种不同风格的数组。向量是下限为0的一维数组,其余的统称为数组(array)。向 量的性能更佳,是C#中最常用的。 T[][] 形式的数组仍然为向量,只不过元素类型为 T[] ;只有 C#中的矩形数组,如 string[10, 20] ,属于CLR术语中的数组。在C#中,你不能直接创建非 零下限的数组——需要使用 Array.CreateInstance 来创建,它可以分别指定下限、长度和元 素类型。如果创建了非零下限的一维数组,就无法将其成功转换为 T[] ——这种强制转换可以通 过编译,但会在执行时失败。

  C#编译器在很多方面都内嵌了对数组的支持。它不仅知道如何创建数组及其索引,还可以在 foreach 循环中直接支持它们;在使用表达式对编译时已知为数组的类型进行迭代时,将使用 Length 属性和数组索引器,而不会创建迭代器对象。这更高效,但性能上的区别通常忽略不计。

  与 List<T> 相同,数组支持 ConvertAll 、 FindAll 和 BinarySearch 方法,不过对数组来 说,这些都是 Array 类的以数组为第一个参数的静态方法。

  回到本节最开始所说的,数组是相当低级的数据结构。它们是其他集合的重要根基,在适当 的情况下有效,但在大量使用之前还是应该三思。Eric同样为该话题撰写了博客,指出它们有“些 许害处”(参见http://mng.bz/3jd5)。我不想夸大这一点,但在选择数组作为集合类型时,这是一 个值得注意的缺点。

  ① 容易混淆的是,也可以将 Stream[] 隐式转换为 IList<Object> ,尽管 IList<T> 本身是不变的。

B.2.3  LinkedList<T>

  什么时候列表不是list呢?答案是当它为链表的时候。 LinkedList<T> 在很多方面都是一个 列表,特别的,它是一个保持项添加顺序的集合——但它却没有实现 IList<T> 。因为它无法遵 从通过索引进行访问的隐式契约。它是经典的计算机科学中的双向链表:包含头节点和尾节点, 每个节点都包含对链表中前一个节点和后一个节点的引用。每个节点都公开为一个 LinkedListNode<T> ,这样就可以很方便地在链表的中部插入或移除节点。链表显式地维护其 大小,因此可以访问 Count 属性。

  在空间方面,链表比维护后台数组的列表效率要低,同时它还不支持索引操作,但在链表中的 任意位置插入或移除元素则非常快,前提是只要在相关位置存在对该节点的引用。这些操作的复杂 度为O(1),因为所需要的只是对周围的节点修改前/后的引用。插入或移除头尾节点属于特殊情况, 通常可以快速访问需要修改的节点。迭代(向前或向后)也是有效的,只需要按引用链的顺序即可。

  尽管 LinkedList<T> 实现了 Add 等标准方法(向链表末尾添加节点),我还是建议使用显式 的 AddFirst 和 AddLast 方法,这样可以使意图更清晰。它还包含匹配的 RemoveFirst 和 RemoveLast 方法,以及 First 和 Last 属性。所有这些操作返回的都是链表中的节点而不是节点 的值;如果链表是空(empty)的,这些属性将返回空(null)。

B.2.4 Collection<T> 、 BindingList<T> 、 ObservableCollection<T> 和 KeyedCollection<TKey, TItem>

  Collection<T> 与我们将要介绍的剩余列表一样,位于 System.Collections.Object- Model 命名空间。与 List<T> 类似,它也实现了泛型和非泛型的集合接口。

   尽管你可以对其自身使用 Collection<T> ,但它更常见的用法是作为基类使用。它常扮演 其他列表的包装器的角色:要么在构造函数中指定一个列表,要么在后台新建一个 List<T> 。所 有对于集合的变动行为,都通过受保护的虚方法( InsertItem 、 SetItem 、 RemoveItem 和 ClearItems )实现。派生类可以拦截这些方法,引发事件或提供其他自定义行为。派生类可通 过 Items 属性访问被包装的列表。如果该列表为只读,公共的变动方法将抛出异常,而不再调用 虚方法,你不必在覆盖的时候再次检查。 BindingList<T> 和 ObservableCollection<T> 派生自 Collection<T> ,可以提供绑定 功能。

  BindingList<T> 在.NET 2.0中就存在了,而 ObservableCollection<T> 是WPF (Windows Presentation Foundation)引入的。当然,在用户界面绑定数据时没有必要一定使用它 们——你也许有自己的理由,对列表的变化更有兴趣。这时,你应该观察哪个集合以更有用的方 式提供了通知,然后再选择使用哪个。注意,只会通知你通过包装器所发生的变化;如果基础列 表被其他可能会修改它的代码共享,包装器将不会引发任何事件。

   KeyedCollection<TKey, TItem> 是列表和字典的混合产物,可以通过键或索引来获取项。 与普通字典不同的是,键不能独立存在,应该有效地内嵌在项中。在许多情况下,这很自然,例 如一个拥有 CustomerID 属性的 Customer 类型。 KeyedCollection<,> 为抽象类;派生类将实 现 GetKeyForItem 方法,可以从列表中的任意项中提取键。在我们这个客户的示例中, GetKeyForItem 方法返回给定客户的ID。与字典类似,键在集合中必须是唯一的——试图添加 具有相同键的另一个项将失败并抛出异常。尽管不允许空键,但 GetKeyForItem 可以返回空(如 果键类型为引用类型),这时将忽略键(并且无法通过键获取项)。

B.2.5  ReadOnlyCollection<T> 和 ReadOnlyObservableCollection<T>

  最后两个列表更像是包装器,即使基础列表为易变的也只提供只读访问。它们仍然实现了泛型和非泛型的集合接口。并且混合使用了显式和隐式的接口实现,这样使用具体类型的编译时表达式的调用者将无法使用变动操作。

  ReadOnlyObservableCollection<T> 派生自 ReadOnlyCollection<T> ,并和 Observerble- Collection<T> 一样实现了相同的 INotifyCollectionChanged 和 INotifyPro pertyChanged 接口。 ReadOnlyObservableCollection<T> 的实例只能通过一个 ObservableCollection<T> 后台列表进行构建。尽管集合对调用者来说依然是只读的,但它们可以观察对后台列表其他地方的 改变。

  尽管通常情况下我建议使用接口作为API中方法的返回值,但特意公开 ReadOnly- Collection<T> 也是很有用的,它可以为调用者清楚地指明不能修改返回的集合。但仍需写明 基础集合是否可以在其他地方修改,或是否为有效的常量。

原文地址:https://www.cnblogs.com/kikyoqiang/p/10187341.html