C#容器源码解析之字典

7 篇文章 0 订阅
1 篇文章 0 订阅

前言

容器类是日常开发中最经常使用的类,常用的容易有List,Queue,Stack,HashSet以及Dictionary,而今天我们就一起看看Dictionary底层的源码实现吧。(这里的容器类指的是泛型的容器类)

源码链接:dictionary.cs (microsoft.com)

解析

成员变量

首先先看看主要定义的成员变量有哪些以及它们的作用又是什么。

  1.  buckets是一个整形数组。对于字典既HashMap,我相信大部分人都大概知道他的一些基本原理,及通过数据的哈希值对数组长度做取模运算找到其存放位置,而buckets就是这个存放的数组。不过它不是直接存储数据,而是存放该哈希值的数据头,相当于数组每个元素都是存放了一个该hash值的链表,这里应该就会有人疑惑一个整形数组怎么存储一个链表,我们到后面具体实现再说。
  2. entries是一个Entry的数组。Entry结构体我们可以看见其中记录了一个元素的hashCode(哈希值,后续都以hashCode表示),他的下一个节点next,该元素的key和该元素的value。所以这个变量主要是将插入的元素做一次封装在存储。
  3. count是一个整形变量。很容易看出他是表示字典内元素的个数。
  4. version是一个整形变量。他表示的是一个版本,每一次会影响数据长度的操作都会更新版本,主要是用来防止在foreach迭代时去修改元素的,相信很多人都知道在foreach是不能添加或删除元素的,一旦怎么操作就会报错就是通过这个字段检测的。不过这个字段和字典底层实现没有多少关系。
  5. fressList是一个整形变量。字典中属于空闲空间会形成一个链表,他表示是首个空闲空间的位置及链表头。
  6. comparer是一个比较器。它表示字典内元素毕竟的方式,内部定义了一个比较相等方和获取hashCode的方法。
  7. keys是一个Key的容器。它就是所有key的集合。
  8. values是一个Value的容器。它是所有value的集合。

实现方法

Add方法

实现是通过Insert方法,所以我们再看看Insert方法 ,代码很多,我们调重点说。

 

三个参数,参数一是插入的key,二是插入的值,三是是否是添加元素主要用于判断插入还是修改。

判断buckets是否为空,为空初始化空间0。我们再看看Initialize方法

首先获取一个比capacity大的最小质数作为桶的空间大小。然后初始化桶的值为-1表示没有存储元素,fressList为-1表示没有空闲空间。

 

 

 获取元素hashCode,计算他在buckets中的位置。定义哈希冲突次数,后面使用。

buckets[i]表示再此hashCode链表的头结点。遍历此hashCode链表,如果hashCode相同且key的值也相同则为相同元素,如果是修改节点则直接修改节点退出,否则报错提示不能查询相同的key。如果不是相同元素则移动到链表下一个元素且让哈希冲突次数加一。

 

这一段是最难理解的部分,所以可能要多看几次。

如果有空闲容量 的话,插入entries数组的空闲位置,空闲链表移动到下一位,空闲容量减一。

否则先判断是否扩容,如果需要扩容则扩容后重新计算再buckets中的位置。

扩容的具体实现

 获取比当前容量大的第一个质数作为新容量,并且不重新计算hashCode。

具体实现:创建新容量的buckets,如果需要重新计算hashCode则遍历entries数组重新计算hashCode,然后再遍历一次entries数组重新计算再buckets中的位置。替换原来的buckets和entries。

回到插入实现,扩容完后将插入位置定为容量尾部。如果字典只做插入操作的化,插入的元素都是按顺序插入进entries中的,所以如果只做插入,遍历字典你会发现是有序的,就是通过这种方式实现的。之所以插尾部就是因为扩容了,所以新元素所在entries中的位置一定就是元素长度位置。

然后就是给插入entries数组所在位置的entry赋值了。

 然后就是解决哈希冲突过大问题,官方定义的是如果冲突数上限是100,然后判断它的比较器是否满足条件,条件基本就是如果没有指定比较器,或者比较器为默认比较器或者比较器是实现了可以随机获取hash算法的比较器时,就换一个比较器(主要就是切换hashCode的算法),然后重新计算所有元素hashCode和位置。

Remove方法

如果buckets为空,就一定没有值,直接返回false。

否则计算hashCode和再buckets中的位置bucket,遍历该hashCode的链表,如果hashCode一样且key的值一样的话则进行移除操作,如果移除的是头节点则让buckets记录改节点的下一个节点作为头结点。如果不是头节点则让上一个节点指向自己的下一个节点,和链表移除操作一样。然后初始化改entry的值,并把当前节点的下一个节点指向空闲节点的头结点,空闲节点指向现在的节点,及链表中的头插法。然后空闲空间加一,版本加一。

总结

这里主要讲了两个主要方法的实现,看懂这两个方法其它方法就很好看懂了。值得注意的是字典的容量始终是保持质数的,主要就是为了减少哈希冲突的次数所设计。然后就是存储hashCode相同节点的链表和存储空闲空间的链表虽然是链表的思路,但是具体实现是通过数组实现的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值