第 1 章 — Collections 类、泛型类
1.1 群集(collection) 的定义
群集是一种结构化的数据类型。
群集可以分为两类:线性的和非线性的。
线性群集是一张元素列表,表中的元素顺次相连。线性群集中的元素通常由位置来决定次序(例如,第一个元素、第二个元素、第三个元素,依次类推)。在现实世界中,购物清单就是很好的线性群集实例。而在计算机世界中(当然这也是真实世界)则把数组设计成线性群集。
非线性群集所包含的元素在群集内没有位置次序之分。组织结构图就像用架子垒好的台球一样是一个非线性群集的实例。而在计算机世界中树、堆、图和集都是非线性群集。
群集 Count 就是群集属性的一个实例。它保存着群集中数据项的数量。这里把群集的操作称为方法,它包括 Add(即向群集添加新元素),Insert(即在群集指定的索引位置添加新元素)、Remove(即从群集中移除指定元素)、Clear(即从群集中移除所有元素)、Contains(即确定指定元素是否是群集的成员)、以及 IndexOf(即确定指定元素在群集中的索引位置)。
1.2 群集(collection) 的描述
在两种主要的群集类中有几个子类别。线性的群集可能是直接存取群集,也可能是顺序存取群集。而非线性的群集既可以是层次群集,也可以是组群集。
1.2.1 直接存取群集(collection)
直接存取群集最常见的实例就是数组。
数组可以是静态的,这样当声明数组的时候便于针对程序的长度来固定指定元素的数量。数组也可以是动态的,通过 ReDim 或者 ReDim Preserve 语句就可以增加数组元素的数量。
在 C#语言中,数组不只是内置的数据类型,它还是一种类。
我们可以用数组来存储一个线性的群集。向数组添加新元素是很容易的,只要简单地把新元素放置在数组尾部第一个空位上就可以了。但是,在数组中插入一个元素就不是这么容易的(或高效)了。因为要给插入的元素空出位置,所以需要按顺序向后移动数组元素。从数组的尾部删除一个元素也是很有效率的操作,只要简单地移除掉最后一个元素的值就可以了。但是,删除数组中任何其他位置上的元素就没有这么有效率了,就像处理插入操作一样,为了保持数组中元素的连续性,可能需要先前调整许多数组元素的位置。
字符串是直接存取群集的另外一种类型。字符串是字符的群集。和存取数组元素的方式一样,也可以基于字符的索引对其进行存取。在 C#语言中,字符串也是作为类对象来实现的。这个类包含一个在字符串上执行标准操作的庞大的方法集合,其中操作有串连接、返回子串、插入字符、移除字符等等。
C#字符串是不可变的。这意味着一旦对字符串进行了初始化,就不能再改变它了。当要修改字符串的时候,不是改变原始的字符串,而是创建一个字符串的副本。在某些情况下这种行为可能会导致性能下降。
C#语言的结构所增加的强大能力就是为执行存储在数据上的操作定义了方法。尽管不能从结构继承或推导出一种新的类型,但是这种做法使得结构在某些地方很像一个类。
1.2.2 顺序存取群集 Sequential Access Collections
由于不能直接存取线性表的元素,为了访问某个元素就需要遍历线性表直到到达要找元素的位置为止。线性表的实现通常允许两种遍历表的方法:一种是单向从前往后遍历,而另一种则是双向遍历,即从前向后和从后先前遍历。
线性表的某些类型限制访问数据元素。这类线性表有堆栈和队列。堆栈是一种只允许在表头(或顶端)存取数据的表。在表的顶端放置数据项,而且也只能从表的顶端移出数据项。正是基于这种原因,堆栈也被称为后进先出结构。这里把向堆栈添加数据项的操作称为入栈,而把从堆栈移出数据项的操作称为出栈。
队列是一种只允许在表尾进行数据项添加和移出操作的表。它也被称为是先进先出结构。这里把向队列添加数据项称为 EnQueue,而把从队列移出数据项称为 DeQueue。
最后要讨论的一类线性群集被称为通用的索引群集。这类群集的第一种就是散列表。它存储了一组与关键字相关联的数据值。在散列表中有一个被称为散列函数的特殊函数。此函数会取走一个数据值,并且把此数据值(称为关键字)转换成用来取回数据的整数索引。然后此索引会用来存取访问与关键字相关联的数据记录。C#语言有一个称为 HashTable 的类用来存储散列表的数据。
另外一种通用的索引群集就是字典。字典也被称为联合,它是由一系列键值对构成的。此结构与词典类似,词典中的词是关键字,而词的定义则是与关键字相关联的值。关键字就是与其相关联的值内的索引。虽然索引不需要就是整数,但是由于上述这种索引方案,所以还是常把字典称为联合数组。
1.2.3 层次群集
非线性群集分为两大主要类型:层次群集和组群集。层次群集是一组划分了层次的数据项集合。位于某一层的数据项可能会有位于下一较低层上的后继数据项。
树是一种常见的层次群集。树群集看上去像是一棵倒立的树,其中一个数据项作为根,而其他数据值则作为叶子挂在根的下面。树的元素被称为节点,而且在特定节点下面的元素被称为是此节点的孩子。
二叉树是树群集的一种特殊类型,树中每个节点最多只有两个孩子。二叉树可以变成二叉查找树,这样做可以极大地提高查找大量数据的效率。实现的方法是依据从根到要存储数据的节点的路径为最短路径的方式来放置节点。还有一种树类型就是堆。堆这样组织就是为了便于把最小数据值始终放置在根节点上。在删除时会移除根节点。此外,堆的插入和删除操作总是会导致堆的重组,因为只有这样才能把最小值放在根节点上。我们经常会用堆来排序,这被称为是堆排序。通过反复删除根节点以及重组堆的方式就可以对存储在堆内的数据元素进行排序。
1.2.4 组群集
数据项为无序的非线性群集被称为组。集合、图和网络是组群集的三种主要类型。集合是一种无序数据值的群集,并且集合中每一个数据值都是唯一的。当然,就像整数一样,班级中学生的列就是一个集合的实例。在集合上执行的操作包括联合和交叉。
1.3 CollectionBase
.NET 框架库不包括用于存储数据的通用 Collection 类,但是大家可以使用一种抽象的类 CollectionBase 类来构造属于自己的 Collection 类。CollectionBase 类为程序员提供了实现定制 Collection 类的能力。CollectionBase 类隐含实现了两个为构造 Collection 类所必需的接口,即 ICollection 和 IEnumerable,而留给程序员要做的工作就只是对这些作为 Collection 类特殊内容的方法的实现。
1.3.1 用 ArrayLists 实现 Collection
1.3.2 定义 Collection 类
在 C#语言中定义一个Collection 类最简单的方法就是把在 System.Collections库中已找到的抽象类CollectionBase类作为基础类。此类提供了一套可以实现构造自身群集的抽象方法集合。CollectionBase 类还提供了一种基础的数据结构—InnerList(一个 ArrayList)。此结构可以用作自身类的基础。如何使用 CollectionBase 来构造Collection 类。
1.3.3 实现 Collection 类
弥补 Collection 类的全部方法包括一些与类的基础数据结构 InnerList 相交互的类型。第一部分要实现的方法是 Add 方法、Remove 方法、Count 方法和 Clear 方法。尽管定义的其他方法可以使类更有用,但是上述这些方法是类的绝对基本要素。
using System;
using System.Collections;
class Program
{
static void Main(string[] args)
{
Collection names = new Collection();
names.Add("names1");
names.Add("names2");
names.Add("names3");
names.Add("names4");
foreach (var name in names)
{
Console.WriteLine(name);
}
Console.WriteLine(names.Count());
names.Remove("names1");
Console.WriteLine(names.Count());
names.Clear();
Console.WriteLine(names.Count());
}
}
class Collection : CollectionBase
{
public void Add(Object item)
{
InnerList.Add(item);
}
public void Remove(Object item)
{
InnerList.Remove(item);
}
public new void Clear()
{
InnerList.Clear();
}
public new int Count()
{
return InnerList.Count;
}
}
1.4 泛型编程
面向对象编程的问题之一就是所谓“代码膨胀”的特征。为了说明方法参数所有可能的数据类型而需要重载某种方法或重载一套方法集合的时候,就会发生某种类型的代码膨胀。代码膨胀的解决方案之一就是使某个值呈现多种数据类型的能力,同时仅提供此值的一种定义。这种方法被称为是泛型编程。泛型编程提供数据类型“占位符”。它在编译时由特定的数据类型填充。这个占位符用一对尖括号(< >)和放在括号间的标识符来表示。