范型集合
毫无疑问,范型最典型的应用莫过于范型集合了。在 .NET 2.0 中提供了已有集合类和接口的范型版本,它们位于 System.Collections.Generic 命名空间中。
.NET 2.0 中新的范型集合类并不是简单的在已有非范型集合类的设计上多加了个范型参数 T 而已。新的范型集合类的设计充分吸收了已有设计中的合理之处并摒弃了一些不甚合理之处,同时引入了新的针对范型的设计。所以,新的范型类和接口的设计应该更加合理和有效,不过 .NET 程序员则需要花些时间学习新的设计并了解与已有设计有什么样的不同,以及在将代码从非范型集合移植到范型集合时可能会出现的兼容性问题。
下面是范型集合和已有非范型集合的对照表(不全):
非范型接口 |
范型接口 |
非范型类 |
范型类 |
IEnumerator |
IEnumerator<T> |
ArrayList |
List<T> |
IEnumerable |
IEnumerable<T> |
Stack |
Stack<T> |
ICollection |
ICollection<T> |
Queue |
Queue<T> |
IList |
IList<T> |
DictionaryEntry |
KeyValuePair<K, V> |
IDictionary |
IDictionary<T> |
Hashtable |
Dictionary<K, V> |
IComparable |
IComparable<T> |
Comparer |
Comparer<T> |
IComparer |
IComparer<T> |
|
|
可以看到,部分类的名字做了修改,例如 ArrayList 现在改为 List<T>,Hashtable 改为 Dictionary<K, V>,DictionaryEntry 改为 KeyValuePair<K, V> 等等。这样的命名当然更加合理(因为 IList<T> 是接口,List<T> 是对应的具体类;同样 IDictionary<K, V> 是接口, Dictionary<K, V> 是对应的具体类;而 KeyValuePair<K, V> 显然比 DictionaryEntry 更加容易理解和记忆),但对已经习惯了以前的命名的程序员来说可能一开始会有点找不找北的感觉。
前面说过,新的范型集合接口/类和以前的非范型版本相比有较大的设计改变,下面我们来看看这些变化。
IEnumerator<T>
IEnumerator/IEnumerator<T> 接口允许对一个集合进行遍历,主要用在 .NET 编程语言的遍历语句中,例如 C# 的 foreach 语句。用户代码通常不直接使用这个接口。IEnumerator<T> 和非范型版本 IEnumerator 相比去掉了 Reset 方法。这可能是出于以下考虑:
l IEnumerator<T> 接口主要设计用于支持诸如 foreach 这样的语句,而这些地方用不到 Reset 方法。去掉 Reset 方法使得设计更加简化并降低了实现该接口的难度。如果调用者需要类似 Reset 的功能,可以重新获取一个枚举器(例如通过调用 GetEnumerator 方法)。
l C# 2.0 Iterators 提供了自动生成枚举器的方法,编译器自动为指定的类实现 IEnumerator 接口和 IEnumerator<T> 接口。而对IEnumerator 接口的 Reset 方法的实现只是简单的抛出 System.NotSupportedException 异常。所以在 IEnumerator<T> 的设计中移去 Reset 方法显得非常自然和合理。
ICollection<T>
ICollection<T> 接口的设计和非范型版本 ICollection 相比改变很大。ICollection 接口的最初设计意图是支持复制集合元素(通过 Count 属性和 CopyTo 方法),以及支持同步访问模式(通过 IsSynchronized 属性和 SyncRoot 属性)。ICollection<T> 的设计保留了对复制集合元素的支持,但是摒弃了对同步访问模式的支持,这是因为实践证明 ICollection 的同步访问模式是让人困惑和低效的。不少刚学 .NET 的程序员一开始搞不懂 SyncRoot 是个什么东东,有什么用。另外,从性能和逻辑上考虑,何时锁定集合应该由调用者决定,而不是由实现者决定。所以总的来说 IsSynchronized 和 SyncRoot 不是很理想的设计。因此,ICollection<T> 没有 IsSynchronized 属性和 SyncRoot 属性。
除此之外,ICollection<T> 还增加了一些新的属性和方法,它们让 ICollection<T> 接口变得更加有用。这些属性和方法事实上是从 IList 和 IDictionary 的共同属性和方法移植过来的,包括:
l IsReadOnly,用于判断集合是否是只读的。
l Add/Remove/Clear,用于对集合元素进行管理。这些方法对列表和字典都是有效的。
l Contains,用于判断集合中是否包含指定的值。
另外,对于一些不需要更改集合的使用情景来说,提供一个类似 IReadOnlyCollection<T> 这样的接口可能会有意义,它只需要支持 Count 属性,CopyTo 方法和 Contains 方法即可。然而微软并没有采用这样的设计,主要理由是为了使基本集合接口尽量简单和易用。微软的建议是程序员需要的话自己定义这样的接口。
IList<T> 和 List<T>
刚才提到,IList<T> 相对于 IList 的变化是通用的属性和方法被移植入 ICollection<T> 了,仅剩下对列表有效的基于索引访问的属性和方法。
List<T> 相对 ArrayList 来讲也做了很大的设计改变。做出这些改变的主要考虑是性能,因为动态数组是 .NET 程序使用的最基本的数据结构之一,它的性能影响到应用程序的全局。例如,以前 ArrayList 默认的 Capacity 是 16,而 List<T> 的默认 Capacity 是 4,这样可以尽量减小应用程序的工作集。另外,List<T> 的方法不是虚拟方法(ArrayList 的方法是虚拟方法),这样可以利用函数内联来提高性能(虚函数不可以被内联)。List<T> 也不支持问题多多的 Synchronized 同步访问模式。
List<T> 相比 ArrayList 增加的一个重要特性则是对 Functional Programming 的支持。我们将在 Functional Programming 部分介绍这一新特性。
IDictionary<K, V> 和 Dictionary<K, V>
IDictionary<K, V> 和 Dictionary<K, V> 相比非范型版本一个很大的变化是当指定的键不存在时索引器的处理逻辑。对 IDictionary 和 Hashtable 来说,值的存储类型是 object,当键不存在时,索引器将返回 null,当键存在而对应值为 null 的话也返回 null(设计者可能认为调用者通常关心的是值是不是有效,而不是区分这两种情况)。然而,对于范型版本来说,因为存储的可能是值类型,所以不可以返回 null 来作为键不存在的标识。因此, IDictionary<K, V>和 Dictionary<K, V> 的索引器在指定键不存在的情况下将抛出 KeyNotFoundException 异常。这将导致源代码级别的不兼容,也就是说,以下的代码在存储值类型的情况下将不可移植,而必须改写(例如先使用 ContainsKey 方法判断指定键是否存在,然后再访问;或者使用不抛出异常的 TryGetValue 方法):
Hashtable map = ...;
if (map[“s1”] == null) { // 如果是范型版本将抛出异常而不是返回null
...
}
这一问题反映了设计者在最初设计 Hashtable 类的时候考虑的并不是很周到——使用了魔术值 null,既可以是指键不存在的情况,也可以是键存在而值为 null 的情况,而这一点对范型是不成立的。另外,从 Design By Contract 的角度讲,当指定键值不存在时,抛出异常是很自然的事情(与是否使用范型无关),就像数组越界一样。估计原设计者主要是从性能角度考虑才使用了 null 而不是异常处理。
IComparable<T>,IComparer<T> 和Comparer<T>
这几个接口/类用于比较和排序。IComparable<T> 相比 IComparable 添加了 Equals 方法,当然也是为了尽量减少 boxing(对于值类型类说)。IComparer<T> 相对 IComparer 则不仅添加了 Equals 方法,而且还新增加了 GetHashCode 方法。咋看一下似乎和比较不太相关,但事实上,对于字符串来说,比较和哈希值是息息相关的。在以前的非范型设计中,需要同时使用 IComparer 和 IHashCodeProvider 两个接口,例如 Hashtable 的构造函数:
public Hashtable(IHashCodeProvider hcp, IComparer comparer);
其中 IHashCodeProvider 和 IComparer 两个参数必须匹配(例如都使用 InvariantCultureIgnoreCase),否则结果会不正确。为了让程序员能够快速的编写出正确的代码,现在的 IComparer<T> 把比较和生成哈希代码的功能集成在一起,例如 Dictionary<K, V> 的构造函数:
public Dictionary(IComparer<K, V> comparer);
Comparer<K, V> 提供了 IComparer<K, V> 的默认实现,微软最新的设计指南建议使用 Comparer<K, V> 而不是其他方法来比较和排序。
另外,.NET 2.0 中新添加了一个字符串比较类——StringComparer,位于 System 命名空间。StringComparer 不是一个范型类,不过它实现了 IComparer<string> 接口,对于需要提供大小写无关的字符串比较很有用。例如,下面的代码创建了一个大小写无关的字典:
Dictionary<string, int> dict = new Dictionary<string, int>(StringComparer.InvariantCultureIgnoreCase);
dict[“Test”] = 10;
int n = dict[“test”];