.NET 2.0 基础类库中的范型——范型集合

范型集合

毫无疑问,范型最典型的应用莫过于范型集合了。在 .NET 2.0 中提供了已有集合类和接口的范型版本,它们位于 System.Collections.Generic 命名空间中。

.NET 2.0 中新的范型集合类并不是简单的在已有非范型集合类的设计上多加了个范型参数 T 而已。新的范型集合类的设计充分吸收了已有设计中的合理之处并摒弃了一些不甚合理之处,同时引入了新的针对范型的设计。所以,新的范型类和接口的设计应该更加合理和有效,不过 .NET 程序员则需要花些时间学习新的设计并了解与已有设计有什么样的不同,以及在将代码从非范型集合移植到范型集合时可能会出现的兼容性问题。

下面是范型集合和已有非范型集合的对照表(不全):

非范型接口

|

范型接口

|

非范型类

|

范型 类

---|---|---|---

IEnumerator

|

IEnumerator

  1<t>
  2
  3| 
  4
  5ArrayList 
  6
  7| 
  8
  9List<t>  
 10  
 11IEnumerable 
 12
 13| 
 14
 15IEnumerable<t>
 16
 17| 
 18
 19Stack 
 20
 21| 
 22
 23Stack<t>  
 24  
 25ICollection 
 26
 27| 
 28
 29ICollection<t>
 30
 31| 
 32
 33Queue 
 34
 35| 
 36
 37Queue<t>  
 38  
 39IList 
 40
 41| 
 42
 43IList<t>
 44
 45| 
 46
 47DictionaryEntry 
 48
 49| 
 50
 51KeyValuePair<k, v="">  
 52  
 53IDictionary 
 54
 55| 
 56
 57IDictionary<t>
 58
 59| 
 60
 61Hashtable 
 62
 63| 
 64
 65Dictionary<k, v="">  
 66  
 67IComparable 
 68
 69| 
 70
 71IComparable<t>
 72
 73| 
 74
 75Comparer 
 76
 77| 
 78
 79Comparer<t>  
 80  
 81IComparer 
 82
 83| 
 84
 85IComparer<t>
 86
 87| 
 88
 89|   
 90  
 91可以看到,部分类的名字做了修改,例如  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  更加容易理解和记忆),但对已经习惯了以前的命名的程序员来说可能一开始会有点找不找北的感觉。 
 92
 93前面说过,新的范型集合接口  /  类和以前的非范型版本相比有较大的设计改变,下面我们来看看这些变化。 
 94
 95###  IEnumerator<t>
 96
 97IEnumerator/IEnumerator<t> 接口允许对一个集合进行遍历,主要用在  .NET  编程语言的遍历语句中,例如  C#  的  foreach  语句。用户代码通常不直接使用这个接口。  IEnumerator<t> 和非范型版本  IEnumerator  相比去掉了  Reset  方法。这可能是出于以下考虑: 
 98
 99l  IEnumerator<t> 接口主要设计用于支持诸如  foreach  这样的语句,而这些地方用不到  Reset  方法。去掉  Reset  方法使得设计更加简化并降低了实现该接口的难度。如果调用者需要类似  Reset  的功能,可以重新获取一个枚举器(例如通过调用  GetEnumerator  方法)。 
100
101l  C# 2.0 Iterators  提供了自动生成枚举器的方法,编译器自动为指定的类实现  IEnumerator  接口和  IEnumerator<t> 接口。而对  IEnumerator  接口的  Reset  方法的实现只是简单的抛出  System.NotSupportedException  异常。所以在  IEnumerator<t> 的设计中移去  Reset  方法显得非常自然和合理。 
102
103###  ICollection<t>
104
105ICollection<t> 接口的设计和非范型版本  ICollection  相比改变很大。  ICollection  接口的最初设计意图是支持复制集合元素(通过  Count  属性和  CopyTo  方法),以及支持同步访问模式(通过  IsSynchronized  属性和  SyncRoot  属性)。  ICollection<t> 的设计保留了对复制集合元素的支持,但是摒弃了对同步访问模式的支持,这是因为实践证明  ICollection  的同步访问模式是让人困惑和低效的。不少刚学  .NET  的程序员一开始搞不懂  SyncRoot  是个什么东东,有什么用。另外,从性能和逻辑上考虑,何时锁定集合应该由调用者决定,而不是由实现者决定。所以总的来说  IsSynchronized  和  SyncRoot  不是很理想的设计。因此,  ICollection<t> 没有  IsSynchronized  属性和  SyncRoot  属性。 
106
107除此之外,  ICollection<t> 还增加了一些新的属性和方法,它们让  ICollection<t> 接口变得更加有用。这些属性和方法事实上是从  IList  和  IDictionary  的共同属性和方法移植过来的,包括: 
108
109l  IsReadOnly  ,用于判断集合是否是只读的。 
110
111l  Add/Remove/Clear  ,用于对集合元素进行管理。这些方法对列表和字典都是有效的。 
112
113l  Contains  ,用于判断集合中是否包含指定的值。 
114
115另外,对于一些不需要更改集合的使用情景来说,提供一个类似  IReadOnlyCollection<t> 这样的接口可能会有意义,它只需要支持  Count  属性,  CopyTo  方法和  Contains  方法即可。然而微软并没有采用这样的设计,主要理由是为了使基本集合接口尽量简单和易用。微软的建议是程序员需要的话自己定义这样的接口。 
116
117###  IList<t> 和  List<t>
118
119刚才提到,  IList<t> 相对于  IList  的变化是通用的属性和方法被移植入  ICollection<t> 了,仅剩下对列表有效的基于索引访问的属性和方法。 
120
121List<t> 相对  ArrayList  来讲也做了很大的设计改变。做出这些改变的主要考虑是性能,因为动态数组是  .NET  程序使用的最基本的数据结构之一,它的性能影响到应用程序的全局。例如,以前  ArrayList  默认的  Capacity  是  16  ,而  List<t> 的默认  Capacity  是  4  ,这样可以尽量减小应用程序的工作集。另外,  List<t> 的方法不是虚拟方法(  ArrayList  的方法是虚拟方法),这样可以利用函数内联来提高性能(虚函数不可以被内联)。  List<t> 也不支持问题多多的  Synchronized  同步访问模式。 
122
123List<t> 相比  ArrayList  增加的一个重要特性则是对  Functional Programming  的支持。我们将在  Functional Programming  部分介绍这一新特性。 
124
125###  IDictionary<k, v=""> 和  Dictionary<k, v="">
126
127IDictionary<k, v=""> 和  Dictionary<k, v=""> 相比非范型版本一个很大的变化是当指定的键不存在时索引器的处理逻辑。对  IDictionary  和  Hashtable  来说,值的存储类型是  object  ,当键不存在时,索引器将返回  null  ,当键存在而对应值为  null  的话也返回  null  (设计者可能认为调用者通常关心的是值是不是有效,而不是区分这两种情况)。然而,对于范型版本来说,因为存储的可能是值类型,所以不可以返回  null  来作为键不存在的标识。因此,  IDictionary<k, v=""> 和  Dictionary<k, v=""> 的索引器在指定键不存在的情况下将抛出  KeyNotFoundException  异常。这将导致源代码级别的不兼容,也就是说,以下的代码在存储值类型的情况下将不可移植,而必须改写(例如先使用  ContainsKey  方法判断指定键是否存在,然后再访问;或者使用不抛出异常的  TryGetValue  方法): 
128
129Hashtable map = ...; 
130
131if (map[“s1”] == null) { //  如果是范型版本将抛出异常而不是返回  null 
132
133... 
134
135} 
136
137这一问题反映了设计者在最初设计  Hashtable  类的时候考虑的并不是很周到——使用了魔术值  null  ,既可以是指键不存在的情况,也可以是键存在而值为  null  的情况,而这一点对范型是不成立的。另外,从  Design By Contract  的角度讲,当指定键值不存在时,抛出异常是很自然的事情(与是否使用范型无关),就像数组越界一样。估计原设计者主要是从性能角度考虑才使用了  null  而不是异常处理。 
138
139###  IComparable<t> ,  IComparer<t> 和  Comparer<t>
140
141这几个接口  /  类用于比较和排序。  IComparable<t> 相比  IComparable  添加了  Equals  方法,当然也是为了尽量减少  boxing  (对于值类型类说)。  IComparer<t> 相对  IComparer  则不仅添加了  Equals  方法,而且还新增加了  GetHashCode  方法。咋看一下似乎和比较不太相关,但事实上,对于字符串来说,比较和哈希值是息息相关的。在以前的非范型设计中,需要同时使用  IComparer  和  IHashCodeProvider  两个接口,例如  Hashtable  的构造函数: 
142
143public Hashtable(IHashCodeProvider hcp, IComparer comparer); 
144
145其中  IHashCodeProvider  和  IComparer  两个参数必须匹配(例如都使用  InvariantCultureIgnoreCase  ),否则结果会不正确。为了让程序员能够快速的编写出正确的代码,现在的  IComparer<t> 把比较和生成哈希代码的功能集成在一起,例如  Dictionary<k, v=""> 的构造函数: 
146
147public Dictionary(IComparer<k, v=""> comparer); 
148
149Comparer<k, v=""> 提供了  IComparer<k, v=""> 的默认实现,微软最新的设计指南建议使用  Comparer<k, v=""> 而不是其他方法来比较和排序。 
150
151另外,  .NET 2.0  中新添加了一个字符串比较类——  StringComparer  ,位于  System  命名空间。  StringComparer  不是一个范型类,不过它实现了  IComparer<string> 接口,对于需要提供大小写无关的字符串比较很有用。例如,下面的代码创建了一个大小写无关的字典: 
152
153Dictionary<string, int=""> dict = new Dictionary<string, int=""> (StringComparer.InvariantCultureIgnoreCase); 
154
155dict[“Test”] = 10; 
156
157int n = dict[“test”];</string,></string,></string></k,></k,></k,></k,></k,></t></t></t></t></t></t></k,></k,></k,></k,></k,></k,></t></t></t></t></t></t></t></t></t></t></t></t></t></t></t></t></t></t></t></t></t></t></k,></k,></k,></t></t></k,></k,></t></t></t></t></k,></t></k,></t></t></t></t></t></t></t>
Published At
Categories with Web编程
Tagged with
comments powered by Disqus