C# 中Hashtable 源码详解

  • 什么是hashtable? 
  • hashtable扩容机制是什么?
  • hashtable是如何解决哈希冲突的?
  • rehash是什么?
  • hashtable是如何进行查找控制的?

HashTable并不是泛型类型,使用object类型会给值类型带来装箱拆箱的压力。

构造函数

HashTable内部维护了一个桶数组,一个桶可以保存一组键值对。

桶数组在初始化时,容量并不一定等于传入的capacity值, 而是会选择一个大于该值的最小质数作为数组大小。

同样的,在进行扩容时,也是先按目前大小×2,然后选择一个大于该结果的最小质数作为新数组容量。

为什么哈希表的大小要用质数呢?

主要是因为一般采用模运算来获取元素存放地址: index = hashcode % length。当容量特别大时倒无所谓,但是哈希表的容量通常又不会特别大,因此采用质数作为模数,也就是容量,会减小哈希冲突的次数。

_loadsize表示的是桶数组容量与负载系数的乘积,负载系数参数需在0.1-1.0之间,但其上限为0.72。

负载系数越小,代表着数据存储越稀疏,查询效率越高,相应的内存占用也越多。

初始化时,负载系数默认是0.72。

 public Hashtable(int capacity, float loadFactor)
        {
            if (capacity < 0)
                throw new ArgumentOutOfRangeException(nameof(capacity), SR.ArgumentOutOfRange_NeedNonNegNum);
            if (!(loadFactor >= 0.1f && loadFactor <= 1.0f))
                throw new ArgumentOutOfRangeException(nameof(loadFactor), SR.Format(SR.ArgumentOutOfRange_HashtableLoadFactor, .1, 1.0));

            // Based on perf work, .72 is the optimal load factor for this table.
            _loadFactor = 0.72f * loadFactor;

            double rawsize = capacity / _loadFactor;
            if (rawsize > int.MaxValue)
                throw new ArgumentException(SR.Arg_HTCapacityOverflow, nameof(capacity));

            // Avoid awfully small sizes
            int hashsize = (rawsize > InitialSize) ? HashHelpers.GetPrime((int)rawsize) : InitialSize;
            _buckets = new bucket[hashsize];

            _loadsize = (int)(_loadFactor * hashsize);
            _isWriterInProgress = false;
            // Based on the current algorithm, loadsize must be less than hashsize.
            Debug.Assert(_loadsize < hashsize, "Invalid hashtable loadsize!");
        }

        private struct bucket
        {
            public object? key;
            public object? val;
            public int hash_coll;   // Store hash code; sign bit means there was a collision.
        }

HashTable中的双重哈希策略

如下代码,非常简洁。

hashcode1 = GetHashCode(key)

hashcode2 = 1 + (hashcode1 * 101 ) % (size - 1)

为什么求出hashcode1要使用uint,并且要& 0x7FFFFFFF 呢?

因为需要将二进制符号位作为碰撞标记。初始化应置为0,发生哈希碰撞时再将其置为1。

        private uint InitHash(object key, int hashsize, out uint seed, out uint incr)
        {
            // Hashcode must be positive.  Also, we must not use the sign bit, since
            // that is used for the collision bit.
            uint hashcode = (uint)GetHash(key) & 0x7FFFFFFF;
            seed = (uint)hashcode;
            // Restriction: incr MUST be between 1 and hashsize - 1, inclusive for
            // the modular arithmetic to work correctly.  This guarantees you'll
            // visit every bucket in the table exactly once within hashsize
            // iterations.  Violate this and it'll cause obscure bugs forever.
            // If you change this calculation for h2(key), update putEntry too!
            incr = (uint)(1 + ((seed * HashHelpers.HashPrime) % ((uint)hashsize - 1)));
            return hashcode;
        }

Remove方法的实现原理

想要理解Add方法的源代码,必须先对Remove操作有一点了解。

首先需要根据Key的哈希值来找到 对象在桶数组中的存储位置。与添加对象时,位置查找算法是基本一致的。怎么添进去的,怎么查出来。

要注意的是,在移除元素时,需要

_buckets[bn].hash_coll &= unchecked((int)0x80000000);

 if (_buckets[bn].hash_coll != 0)   _buckets[bn].key = _buckets;
 else    _buckets[bn].key = null;

这样做的目的是为了保留hash碰撞标记。假如这个位置的元素曾经发生过Hash碰撞,则其key设为_buckets,否则置为null。

 public virtual void Remove(object key)
        {           
            uint hashcode = InitHash(key, _buckets.Length, out uint seed, out uint incr);
            int ntry = 0;

            bucket b;
            int bn = (int)(seed % (uint)_buckets.Length);  
            do
            {
                b = _buckets[bn];
                if (((b.hash_coll & 0x7FFFFFFF) == hashcode) &&
                    KeyEquals(b.key, key))
                {
                    _isWriterInProgress = true;
                   
                    _buckets[bn].hash_coll &= unchecked((int)0x80000000);
                    if (_buckets[bn].hash_coll != 0)
                    {
                        _buckets[bn].key = _buckets;
                    }
                    else
                    {
                        _buckets[bn].key = null;
                    }
                    _buckets[bn].val = null;  
                    _count--;
                    UpdateVersion();
                    _isWriterInProgress = false;
                    return;
                }
                bn = (int)(((long)bn + incr) % (uint)_buckets.Length);
            } while (b.hash_coll < 0 && ++ntry < _buckets.Length);
        }

Add方法的实现原理

了解了Remove方法的实现原理后,我们再来看下Add方法是如何添加元素的。

首先是先判断是否需要扩容,_loadsize并不等于容量,我前面已经介绍了。

其次根据碰撞次数,判断是否需要rehash。

然后再求出key的对应hash值。

那么接下来就是用do...while为添加的键值对找一个合适的位置了,详解见注释,如下代码: 

 private void Insert(object key, object? nvalue, bool add)
        {           
            if (_count >= _loadsize)
            {
                expand();
            }
            else if (_occupancy > _loadsize && _count > 100)
            {
                rehash();
            }
           
            uint hashcode = InitHash(key, _buckets.Length, out uint seed, out uint incr);
            int ntry = 0;
            int emptySlotNumber = -1;
            //先简单使用hashcode1对_buckets.Length作模运算,求出一个地址。
            int bucketNumber = (int)(seed % (uint)_buckets.Length);
            do
            {  
                //(参考上面Remove方法)假如当前地址曾发生过hash碰撞(_buckets[bucketNumber].hash_coll < 0),
                //但该桶目前为空,则暂时将指针指向该地址。
                //因为曾发生过Hash碰撞,所以当前数组中可能存在Hash值相同的其他元素,
                //需要与他们的Key值一一比较             
                if (emptySlotNumber == -1 && (_buckets[bucketNumber].key == _buckets) && (_buckets[bucketNumber].hash_coll < 0))
                    emptySlotNumber = bucketNumber;

                //假如该桶从来为空,或曾有元素但未发生过hash冲突
                if ((_buckets[bucketNumber].key == null) ||
                    (_buckets[bucketNumber].key == _buckets && ((_buckets[bucketNumber].hash_coll & unchecked(0x80000000)) == 0)))
                {
                    
                    if (emptySlotNumber != -1) 
                        bucketNumber = emptySlotNumber;
                  
                    //保存键值与hashcode,然后return
                    _isWriterInProgress = true;
                    _buckets[bucketNumber].val = nvalue;
                    _buckets[bucketNumber].key = key;
                    _buckets[bucketNumber].hash_coll |= (int)hashcode;
                    _count++;
                    UpdateVersion();
                    _isWriterInProgress = false;

                    return;
                }

                //当前桶的key等于要添加的key,判断是否是add操作,是则抛出异常,否则update
                if (((_buckets[bucketNumber].hash_coll & 0x7FFFFFFF) == hashcode) &&
                    KeyEquals(_buckets[bucketNumber].key, key))
                {
                    if (add)
                    {
                        throw new ArgumentException(SR.Format(SR.Argument_AddingDuplicate__, _buckets[bucketNumber].key, key));
                    }
                    _isWriterInProgress = true;
                    _buckets[bucketNumber].val = nvalue;
                    UpdateVersion();
                    _isWriterInProgress = false;

                    return;
                }

                //到这里,说明当前桶不为空,也就是出现了hash碰撞,
                //将碰撞标志位置为1,碰撞计数+1
                if (emptySlotNumber == -1)
                {
                    if (_buckets[bucketNumber].hash_coll >= 0)
                    {
                        _buckets[bucketNumber].hash_coll |= unchecked((int)0x80000000);
                        _occupancy++;
                    }
                }
                // 利用上面求得的hashcode2,也就是incr,重新计算一个新的地址
                bucketNumber = (int)(((long)bucketNumber + incr) % (uint)_buckets.Length);
            } while (++ntry < _buckets.Length);

            //找到合适的地址,将键值对保存到该桶
            if (emptySlotNumber != -1)
            {
                
                _isWriterInProgress = true;
                _buckets[emptySlotNumber].val = nvalue;
                _buckets[emptySlotNumber].key = key;
                _buckets[emptySlotNumber].hash_coll |= (int)hashcode;
                _count++;
                UpdateVersion();
                _isWriterInProgress = false;

                return;
            }
        
            Debug.Fail("hash table insert failed!  Load factor too high, or our double hashing function is incorrect.");
            throw new InvalidOperationException(SR.InvalidOperation_HashInsertFailed);
        }

Rehash的实现原理

 Rehash会在两种情况下出现

  1. 扩容时(改变桶数组长度)
  2. 碰撞次数达到上限(不改变桶数组长度)

当哈希碰撞次数过多时,意味着当前插入查询效率都会降低。

当因为哈希碰撞而Rehash时,桶数组长度并没有改变,hash算法也没发生太大变化,那么此时rehash的意义是什么呢?

答案是为了清理无效的碰撞标记,以及碰撞次数。我们前面讲到了碰撞标记与碰撞次数的维护机制,当有元素被Remove时,二者并不会自动维护成最合理的状态。

详解见代码中的注释: 

 private void rehash(int newsize)
        {
            _occupancy = 0;

            bucket[] newBuckets = new bucket[newsize];

            int nb;
            for (nb = 0; nb < _buckets.Length; nb++)
            {
                bucket oldb = _buckets[nb];
                if ((oldb.key != null) && (oldb.key != _buckets))
                {
                    //碰撞标记重置为0
                    int hashcode = oldb.hash_coll & 0x7FFFFFFF;
                    // putEntry方法的作用就是利用hashcode1查找一个空位插入
                    // 否则碰撞标记置1,while循环,利用hashcode2查找其他空位
                    putEntry(newBuckets, oldb.key, oldb.val, hashcode);
                }
            }

            // 直到添加完成才会替换相关内部状态,包括使用新的桶数组、新的负载上限、更新版本
            // 这样做的目的是为了在rehash时允许并发读取器查看有效的hashtable内容,
            // 以及当分配这个新桶[]时,防止OutOfMemoryException。
            _isWriterInProgress = true;
            _buckets = newBuckets;
            _loadsize = (int)(_loadFactor * newsize);
            UpdateVersion();
            _isWriterInProgress = false;

            Debug.Assert(_loadsize < newsize, "Our current implementation means this is not possible.");
        }

索引器查找实现原理 

查找其实就是根据hashcode,找到对应的桶下标,计算方法与上文所介绍的一致。

不同的是,查找增加了并发控制。使用volatile 关键字声明的_isWriterInProgress 和_version变量将用来保证在进行增删改操作时,读取操作需要等待。

public virtual object? this[object key]
        {
            get
            {
                if (key == null)
                {
                    throw new ArgumentNullException(nameof(key), SR.ArgumentNull_Key);
                }

                bucket[] lbuckets = _buckets;
                uint hashcode = InitHash(key, lbuckets.Length, out uint seed, out uint incr);
                int ntry = 0;

                bucket b;
                int bucketNumber = (int)(seed % (uint)lbuckets.Length);
                do
                {
                    int currentversion;

                   
                    SpinWait spin = default;
                    while (true)
                    {
                        currentversion = _version;
                        b = lbuckets[bucketNumber];

                        if (!_isWriterInProgress && (currentversion == _version))
                            break;

                        spin.SpinOnce();
                    }

                    if (b.key == null)
                    {
                        return null;
                    }
                    if (((b.hash_coll & 0x7FFFFFFF) == hashcode) &&
                        KeyEquals(b.key, key))
                        return b.val;
                    bucketNumber = (int)(((long)bucketNumber + incr) % (uint)lbuckets.Length);
                } while (b.hash_coll < 0 && ++ntry < lbuckets.Length);
                return null;
            }

            set => Insert(key, value, false);
        }

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
课程总体目标:     本级篇面向的学员不再是完全的编程“小白”,而是具备一定C#编程经验,需要进一步查漏补缺、或者需要进一步全面完善自己C#编程知识体系的广大Unity学员。相信通过本级篇的学习,可以使得Unity初级开发人员对于编程语言的掌握更进一步;对于开发大型游戏项目,在编程语言这一层级进一步打下坚实的语言基础。 “级篇”课程讲解特点:       本级篇面向初级游戏研发人员,以及Unity高级学习者。为了更加深入的刨析各个语法的本质,我们采用反编译解读IL间语言的方式,来解构语法难点,使得学员最短时间掌握语法本质。 本课程讲解内容:       C#(for Unity)级篇 在“C#入门”、“基础篇”的基础之上,从以下四个方面着重研究我们游戏开发(包含软件开发)过程C#最重要、最实用的技能模块,使得广大游戏研发人员,对于C#这门Unity脚本有进一步更加完善的认识。一:.Net 框架讲解。    A) .Net 发展历史。    B)  IL  间语言。 CLR  公共语言运行时。    C) 多维数据(常用二维数组)与交错数组。    D) 可变参数 Params    E) 进一步解释“实参”,“形参”。    F) 类的实例化内存分配机制。二:深入学习对象类型    A)  里氏替换原则(LSP)    B)  类的属性极其本质特性    C)  IS ,AS 关键字    D)  深入学习字符串理论        1] 字符串的“驻留性” 原理。        2] 字符串==与Equals() 的本质区别        3] 更多字符串常用方法学习。    E)  枚举类型以及适用场合。三:深入学习集合特性    A)  什么是索引器,以及索引器的适用范围。    B)  学习自定义集合类,以及深入了解Foreach 语句的原理。    C)  深入学习 ArrayList,了解内部存储机制以及原理。    D)  深入学习 HashTable,了解内部存储机制以及原理。    E)  为什么学习泛型集合?    F)  泛型集合与普通集合的性能测试对比实验。    G)  学习“泛型约束”,以及“泛型约束”的适用条件。四:委托与事件        A)  什么是委托,先从讲故事学习起:“老板来啦”!    B)  反编译掌握委托的本质。    C)  委托的四大开发步骤。    D)  什么是事件,以及委托与事件的区别。    E)  事件的常用使用方式。 温馨提示:       本C# for Unity 使用Virtual Studio2012,进行开发与讲解。(学员使用更高版本,对学习没有任何影响) 一、热更新系列(技术含量:高级):A:《lua热更新技术级篇》https://edu.csdn.net/course/detail/27087B:《热更新框架设计之Xlua基础视频课程》https://edu.csdn.net/course/detail/27110C:《热更新框架设计之热更流程与热补丁技术》https://edu.csdn.net/course/detail/27118D:《热更新框架设计之客户端热更框架(上)》https://edu.csdn.net/course/detail/27132E:《热更新框架设计之客户端热更框架()》https://edu.csdn.net/course/detail/27135F:《热更新框架设计之客户端热更框架(下)》https://edu.csdn.net/course/detail/27136二:框架设计系列(技术含量:级): A:《游戏UI界面框架设计系列视频课程》https://edu.csdn.net/course/detail/27142B:《Unity客户端框架设计PureMVC篇视频课程(上)》https://edu.csdn.net/course/detail/27172C:《Unity客户端框架设计PureMVC篇视频课程(下)》https://edu.csdn.net/course/detail/27173D:《AssetBundle框架设计_框架篇视频课程》https://edu.csdn.net/course/detail/27169三、Unity脚本从入门到精通(技术含量:初级)A:《C# For Unity系列之入门篇》https://edu.csdn.net/course/detail/4560B:《C# For Unity系列之基础篇》https://edu.csdn.net/course/detail/4595C: 《C# For Unity系列之级篇》https://edu.csdn.net/course/detail/24422D:《C# For Unity系列之进阶篇》https://edu.csdn.net/course/detail/24465四、虚拟现实(VR)与增强现实(AR):(技术含量:初级)A:《虚拟现实之汽车仿真模拟系统 》https://edu.csdn.net/course/detail/26618五、Unity基础课程系列(技术含量:初级) A:《台球游戏与FlappyBirds—Unity快速入门系列视频课程(第1部)》 https://edu.csdn.net/course/detail/24643B:《太空射击与移动端发布技术-Unity快速入门系列视频课程(第2部)》https://edu.csdn.net/course/detail/24645 C:《Unity ECS(二) 小试牛刀》https://edu.csdn.net/course/detail/27096六、Unity ARPG课程(技术含量:初级):A:《MMOARPG地下守护神_单机版实战视频课程(上部)》https://edu.csdn.net/course/detail/24965B:《MMOARPG地下守护神_单机版实战视频课程(部)》https://edu.csdn.net/course/detail/24968C:《MMOARPG地下守护神_单机版实战视频课程(下部)》https://edu.csdn.net/course/detail/24979
C#,将Hashtable转换为字符串数组可以使用以下代码示例: ```csharp Hashtable hash = new Hashtable(); // 添加键值对 hash.Add("key1", "value1"); hash.Add("key2", "value2"); hash.Add("key3", "value3"); // 创建一个字符串数组 string\[\] array = new string\[hash.Count\]; // 使用CopyTo方法将Hashtable的值复制到字符串数组 hash.Values.CopyTo(array, 0); // 打印字符串数组 foreach (string value in array) { Console.WriteLine(value); } ``` 在上述代码,我们首先创建了一个Hashtable对象,并向其添加了键值对。然后,我们创建了一个与Hashtable大小相同的字符串数组。接下来,我们使用Hashtable的CopyTo方法将所有值复制到字符串数组。最后,我们使用foreach循环遍历字符串数组并打印每个值。 请注意,这里我们只复制了Hashtable的值,如果你需要复制键或键值对,可以使用Hashtable的Keys或DictionaryEntry属性。 #### 引用[.reference_title] - *1* [C# 静态与动态数组](https://blog.csdn.net/lyshark_csdn/article/details/124939204)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [C# Hashtable存入数组、List](https://blog.csdn.net/weixin_30532759/article/details/96505533)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值