字典表示一种非常复杂的数据结构,这种数据结构允许按照某个键来访问元素。字典也称为映射或散列表。字典的主要特征是能根据键来快速查找值。也可以自由地添加和删除元素,这有点像List<T>类,但没有在内存中移动后续元素的性能开销。
键会转化为一个散列。利用散列创建一个数字,它将索引和值关联起来。然后索引包含一个到值的链接。一个索引项可以关联多个值,索引可以存储为一个树形结构。
.NET Framework提供了几个字典类。可以使用的最主要的类是Dictionary<TKey,TValue>。
字典初始化器
C#提供了一个语法,在声明时初始化字典。带有int键和string值的字典可以初始化如下:
var dict = new Dictionary<int, string>()
{
[3] = "three",
[7] = "seven"
};
这里把两个元素添加到字典中。第一个元素的键是3,字符串值是three;第二个元素的键是7,字符串值是seven。这个初始化语法易于阅读,使用的语法与访问字典中的元素相同。
键的类型
用作字典中键的类型必须重写Object类的GetHashCode()方法。只要字典类需要确定元素的位置,它就要调用GetHashCode()方法。GetHadhCode()方法返回的int由字典用于计算在对应位置放置元素的索引。这里不介绍这个算法。我们只需要知道,它涉及素数,所以字典的容量是一个素数。
GetHashCode()方法的实现代码必须满足如下要求:
- 相同的对象应总是返回相同的值。
- 不同的对象可以返回相同的值。
- 它不能抛出异常。
- 它应至少使用一个实例字段。
- 散列代码最好在对象的生存期中不发生变化。
除了GetHashCode()方法的实现代码必须满足的要求之外,最好还满足如下要求:
- 它应执行得比较快,计算的开销不大。
- 散列代码值应平均分布在int可以存储的整个数字范围上。
注:字典的性能取决于GetHashCode()方法的实现代码。
为什么使散列代码值平均分布在整数的取值范围内?如果两个键返回的散列代码值会得到相同的索引,字典类就必须寻找最近的可用空闲位置来存储第二个数据项,这需要进行一定的搜索,以便以后检索这一项。显然这会降低性能,如果在排序时许多键都有相同的索引,这类冲突就更可能出现。根据Microsoft的算法的工作方式,当计算出来的散列代码值平均分布在int.MinValue和int.MaxValue之间时,这种风险会降低到最小。
除了实现GetHashCode()方法之外,键类型还必须实现IEquatable<T>.Equals()方法,或重写Oject类的Equals()方法。因为不同的键对象可能返回相同的散列代码,所以字典使用Equals()方法来比较键。字典检查两个键A和B是否相等,并调用A.Equals(B)方法。这表示必须确保下述条件总是成立:
如果A.Equals(B)方法返回true,则A.GetHashCode()和B.GetHashCode()方法必须总是返回相同的散列代码。
这似乎有点奇怪,但它非常的重要。如果设计出某种重写这些方法的方式,使上面的条件并不总是成立,那么把这个类的实例用作键的字典就不能正常工作,而是会发生有趣的事情。例如,把一个对象放在字典中后,就再也检索不到它,或者试图检索某项,却返回了错误的项。
注:如果为Equals()方法提供了重写版本,但没有提供GetHashCode()方法的重写版本,C#编辑器就会显示一个编译错误。
对于System.Object,这个条件为true,因为Equals()方法只是比较引用,GetHashCode()方法实际上返回一个仅基于对象地址的散列代码。这说明,如果散列表基于一个键,而该键没有重写这些方法,这个散列表就能正常工作。但是,这么做的问题是,只有对象完全相同,键才被认为是相等的。也就是说,把一个对象放在字典中时,必须将它于该键的引用关联起来。也不能在以后用相同的值示例化另一个键的对象。如果没有重写Equals()方法和GetHashCode()方法,在字典中使用类型时就不太方便。
另外,System.String实现了IEquatable接口,并重载了GetHashCode()方法。Equals()方法提供了值的比较,GetHashCode()方法根据字符串的值返回一个散列代码。因此,在字典中把字符串用作键非常的方便。
数字类型(如Int32)也实现了IEquatable接口,并重载了GetHashCode()方法。但是这些类型返回的散列代码只映射到值上