Unity Dictionary底层源码剖析


前言

Dictionary字典型数据结构本质是关键字Key值和Value值一一映射的关系。Key可以是不同的类型,int、string等等,怎么挥事呢?答案是哈希表,是利用哈希函数将键映射到特定位置,以实现快速的键值检索。

砖家建议:先看List源码解析,再看本章。


一、Dictionary源码

变量的定义部分:

public class Dictionary<TKey,TValue>: IDictionary<TKey,TValue>, IDictionary, 
    IReadOnlyDictionary<TKey, TValue>, ISerializable, IDeserializationCallback
{

    private struct Entry {
        public int hashCode;            // 低31位为Hash值,如果未使用则为-1
        public int next;                // 下一个实例索引,如果是最后一个则为-1
        public TKey key;                // 实例的键值
        public TValue value;            // 实例的值
    }

    private int[] buckets;
    private Entry[] entries;
    private int count;
    private int version;
    private int freeList;
    private int freeCount;
    private IEqualityComparer<TKey>comparer;
    private KeyCollection keys;
    private ValueCollection values;
    private Object _syncRoot;
}

Dictionary主要继承了IDictionary接口和ISerializable接口。同时也能看出Dictionary数据结构中和List一样,都是以数组为底层数据结构,所以猜测 扩容操作也是需要的。

Dictionary源码网址为:官方跳转链接

二、Add接口

接口源码如下:

public void Add(TKey key, TValue value)
{
    Insert(key, value, true);
}
private void Initialize(int capacity)
{
    int size = HashHelpers.GetPrime(capacity);
    buckets = new int[size];
    for (int i = 0; i<buckets.Length; i++) buckets[i] = -1;
    entries = new Entry[size];
    freeList = -1;
}

private void Insert(TKey key, TValue value, bool add)
{
    if( key == null) {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
    }

    if (buckets == null) Initialize(0);
    int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
    int targetBucket = hashCode % buckets.Length;

#if FEATURE_RANDOMIZED_STRING_HASHING
    int collisionCount = 0;
#endif

    for (int i = buckets[targetBucket]; i>= 0; i = entries[i].next) {
        if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) {
            if (add) {
                ThrowHelper.ThrowArgumentException(
                    ExceptionResource.Argument_AddingDuplicate);
            }
            entries[i].value = value;
            version++;
            return;
        }

#if FEATURE_RANDOMIZED_STRING_HASHING
        collisionCount++;
#endif
    }
    int index;
    if (freeCount>0) {
        index = freeList;
        freeList = entries[index].next;
        freeCount--;
    }
    else {
        if (count == entries.Length)
        {
            Resize();
            targetBucket = hashCode % buckets.Length;
        }
        index = count;
        count++;
    }

    entries[index].hashCode = hashCode;
    entries[index].next = buckets[targetBucket];
    entries[index].key = key;
    entries[index].value = value;
    buckets[targetBucket] = index;
    version++;

#if FEATURE_RANDOMIZED_STRING_HASHING

#if FEATURE_CORECLR
    // 如果我们触碰到阈值,则需要切换到使用随机字符串Hash的比较器上
    // 在这种情况下,将是EqualityComparer<string>.Default
    // 注意,默认情况下,coreclr上的随机字符串Hash是打开的,所以EqualityComparer<string>.
Default将使用随机字符串Hash

    if (collisionCount>HashHelpers.HashCollisionThreshold && comparer ==
        NonRandomizedStringEqualityComparer.Default)
    {
        comparer = (IEqualityComparer<TKey>) EqualityComparer<string>.Default;
        Resize(entries.Length, true);
    }
#else
    if(collisionCount>HashHelpers.HashCollisionThreshold &&
        HashHelpers.IsWellKnownEqualityComparer(comparer))
    {
        comparer = (IEqualityComparer<TKey>)
            HashHelpers.GetRandomizedEqualityComparer(comparer);
        Resize(entries.Length, true);
    }
#endif // FEATURE_CORECLR

#endif

}

代码很多,简单的分析关键点。首先,Add是由Insert方法代理,加入之前判空后进行数据构造。

if( key == null) {//判空
    ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
}
 if (buckets == null) Initialize(0);//buchets出事为空数组,对其初始化

我们继续看Initialize方法。里面写了如何初始化的,这个和List有点差别,研究一下,发现primes数值是质数。

 int size = HashHelpers.GetPrime(capacity);

HashHelpers,primes数值

public static readonly int[] primes = {
        3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 
            293, 353, 431, 521, 631, 761, 919,
        1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 
            8419, 10103, 12143, 14591,
        17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 
            108631, 130363, 156437,
        187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 
            807403, 968897, 1162687, 1395263,
        1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 
            7199369
};

如果超出这个界限呢,那就返回之前的2倍。如果你创建字典时指定了初始大小,会计算成应该分配的大小。

public static int GetPrime(int min)
{
    if (min<0)
        throw new ArgumentException(
            Environment.GetResourceString("Arg_HTCapacityOverflow"));
    Contract.EndContractBlock();

    for (int i = 0; i<primes.Length; i++)
    {
        int prime = primes[i];
        if (prime>= min) return prime;
    }

    // 如果在我们的预定义表之外,则做硬计算
    for (int i = (min | 1); i<Int32.MaxValue;i+=2)
    {
        if (IsPrime(i) && ((i - 1) % Hashtable.HashPrime != 0))
            return i;
    }
    return min;
}

// 返回要增长到的Hash表的大小
public static int ExpandPrime(int oldSize)
{
    int newSize = 2 * oldSize;

    // 在遇到容量溢出之前,允许Hash表增长到最大可能的大小(约2G个元素)
    // 请注意,即使(item.Length)由于(uint)强制转换而溢出,此检查仍然有效
    if ((uint)newSize>MaxPrimeArrayLength && MaxPrimeArrayLength>oldSize)
    {
        Contract.Assert( MaxPrimeArrayLength == GetPrime(MaxPrimeArrayLength),
            "Invalid MaxPrimeArrayLength");
        return MaxPrimeArrayLength;
    }

    return GetPrime(newSize);
}

初始化之后,对key进行hash处理,得到地址索引。

 int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
 int targetBucket = hashCode % buckets.Length;//取余,确保索引地址在数组长度范围内,否则可能溢出

然后对指定的数组单元格内的链表进行遍历,提取空位置将值填入。拉链法(下有讲解)的链表推入操作。

for (int i = buckets[targetBucket]; i>= 0; i = entries[i].next) {
    if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) {
        if (add) {
            ThrowHelper.ThrowArgumentException(
                ExceptionResource.Argument_AddingDuplicate);
        }
        entries[i].value = value;
        version++;
        return;
    }

#if FEATURE_RANDOMIZED_STRING_HASHING
    collisionCount++;
#endif
}

拉链法是一种解决哈希冲突的方法,主要应用在哈希表中。当不同的关键字经过哈希函数计算后得到相同的哈希地址时,就产生了哈希冲突。拉链法解决冲突的做法是:将所有关键字为同义词的结点链接在同一个单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数组T[0…m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。

如果数组空间不够,也就是代码中freeCount=0时的逻辑进行扩容,扩容后的大小为计算过的大小,通常为之前的2倍。(参考ExpandPrime方法)

 int index;
    if (freeCount>0) {
        index = freeList;
        freeList = entries[index].next;
        freeCount--;
    }
    else {
        if (count == entries.Length)
        {
            Resize();
            targetBucket = hashCode % buckets.Length;
        }
        index = count;
        count++;
    }

    entries[index].hashCode = hashCode;
    entries[index].next = buckets[targetBucket];
    entries[index].key = key;
    entries[index].value = value;
    buckets[targetBucket] = index;

三、Remove接口

接口源码如下:

public bool Remove(TKey key)
{
    if(key == null) {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
    }

    if (buckets != null) {
        int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
        int bucket = hashCode % buckets.Length;
        int last = -1;
        for (int i = buckets[bucket]; i>= 0; last = i, i = entries[i].next) {
            if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].
                key, key)) {
                if (last<0) {
                    buckets[bucket] = entries[i].next;
                }
                else {
                    entries[last].next = entries[i].next;
                }
                entries[i].hashCode = -1;
                entries[i].next = freeList;
                entries[i].key = default(TKey);
                entries[i].value = default(TValue);
                freeList = i;
                freeCount++;
                version++;
                return true;
            }
        }
    }
    return false;
}

移除的原理和Add一样利用了Hash值,再进行余操作,确认索引在数组范围内后,遍历地址进行查找,key值相同则进行移除操作。但是这里为了减少内存频繁操作,直接进行置空。

四、ContainsKey接口

接口源码如下:

public bool ContainsKey(TKey key)
{
    return FindEntry(key)>= 0;
}

private int FindEntry(TKey key)
{
    if( key == null) {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
    }

    if (buckets != null) {
        int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
        for (int i = buckets[hashCode % buckets.Length]; i>= 0; i = 
            entries[i].next) {
            if (entries[i].hashCode ==
                hashCode && comparer.Equals(entries[i].key,key)) return i;
        }
    }
    return -1;
}

这个方法的作用是在哈希表中查找指定键的位置,如果找到则返回该键所在的索引,如果未找到则返回 -1。和Remove的查找方式类似,查找所有冲突列表中与key相当的值。

五、TryGetValue接口

接口源码如下:

public bool TryGetValue(TKey key, out TValue value)
{
    int i = FindEntry(key);
    if (i>= 0) {
        value = entries[i].value;
        return true;
    }
    value = default(TValue);
    return false;
}

TryGetValue接口调用FindEntry方法,其中TValue是对[]操作符的重定义。而TValue中又调用了Insert。

public TValue this[TKey key] {
    get {
        int i = FindEntry(key);
        if (i>= 0) return entries[i].value;
        ThrowHelper.ThrowKeyNotFoundException();
        return default(TValue);
    }
    set {
        Insert(key, value, false);
    }
}

六、哈希函数

综上所述,可以看出哈希冲突的拉链法贯穿了其底层数据结构。所以其中哈希函数决定了字典的效率。
以下是函数创建过程源码:

private static EqualityComparer<T>CreateComparer()
{
    Contract.Ensures(Contract.Result<EqualityComparer<T>>() != null);

    RuntimeType t = (RuntimeType)typeof(T);
    // 出于性能原因专门用字节类型
    if (t == typeof(byte)) {
        return (EqualityComparer<T>)(object)(new ByteEqualityComparer());
    }
    // 如果T implements IEquatable<T>返回一个GenericEqualityComparer<T>
    if (typeof(IEquatable<T>).IsAssignableFrom(t)) {
        return (EqualityComparer<T>)
            RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter(
            (RuntimeType)typeof(GenericEqualityComparer<int>), t);
    }
    // 如果T是一个Nullable<U>从U implements IEquatable<U>返回的NullableEquality
        Comparer<U>
    if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>)) {
        RuntimeType u = (RuntimeType)t.GetGenericArguments()[0];
        if (typeof(IEquatable<>).MakeGenericType(u).IsAssignableFrom(u)) {
            return (EqualityComparer<T>)
                RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((
                RuntimeType)typeof(NullableEqualityComparer<int>), u);
        }
    }

    // 看这个METHOD__JIT_HELPERS__UNSAFE_ENUM_CAST和METHOD__JIT_HELPERS__UNSAFE_
ENUM_CAST_LONG在getILIntrinsicImplementation中的例子
    if (t.IsEnum) {
        TypeCode underlyingTypeCode = Type.GetTypeCode(
            Enum.GetUnderlyingType(t));

        // 根据枚举类型,我们需要对比较器进行特殊区分,以免装箱
        // 注意,我们要对Short和SByte使用不同的比较器,因为对于这些类型,
        // 我们需要确保在实际的基础类型上调用GetHashCode,其中,GetHashCode的实现比其他类型更复杂
        switch (underlyingTypeCode) {
            case TypeCode.Int16: 
                return (EqualityComparer<T>)
                RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((
                RuntimeType)typeof(ShortEnumEqualityComparer<short>), t);
            case TypeCode.SByte:
                return (EqualityComparer<T>)
                RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((
                RuntimeType)typeof(SByteEnumEqualityComparer<sbyte>), t);
            case TypeCode.Int32:
            case TypeCode.UInt32:
            case TypeCode.Byte:
            case TypeCode.UInt16: 
                return (EqualityComparer<T>)
                RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((
                RuntimeType)typeof(EnumEqualityComparer<int>), t);
            case TypeCode.Int64:
            case TypeCode.UInt64:
                return (EqualityComparer<T>)
                    RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((
                    RuntimeType)typeof(LongEnumEqualityComparer<long>), t);
        }
    }
    // 否则,返回一个ObjectEqualityComparer<T>
    return new ObjectEqualityComparer<T>();
}

源码中可以看到有四种处理方式,分别对应byte、IEquatable、IEquatable、underlyingTypeCode四种类型。其中byte和underlyingTypeCode,一个字节、一个数字,容易比较;有IEquatable接口的则使用GenericEqualityComparer获取哈希函数;对于IEquatable来说,如果有Nullable接口就使用NullableEqualityComparer
,如果没有直接使用默认的ObjectEqualityComparer。
项目中想优化,尽量使用数值方式作为键值对。其他方式的Hash值通常使用内存地址比较,消耗较大。

在C#中,所有类指向Object类,比较时没有重写Equals函数时,都是进行内存地址进行比较,消耗较大。

七、线程安全

最后提一点,Dictionary是线程同样是不安全的,需要加锁。但是Hashtable是线程安全的。


总结

Dictionary 是一种常见的数据结构,由数组构成,使用哈希函数将键映射到数组的特定位置。它通过哈希表实现键值对的存储和检索,具有快速的查找、插入和删除操作。在解决哈希冲突方面,Dictionary 使用了拉链法,将具有相同哈希码的键值对存储在同一个链表中,从而保证了高效的冲突解决和性能表现。

  • 15
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Unity中的Dictionary是一种可序列化的字典类,它可以在检查器中进行编辑,无需实现自定义编辑器或属性抽屉。你可以使用任何可序列化的类型作为键或值。 Dictionary的用法有以下几个方面: 1. 实例化:你可以通过以下方式来实例化一个Dictionary: ``` Dictionary<键key, 值value> 名字dic = new Dictionary<键key, 值value>(); Dictionary<Tkey, Tvalue> Dic = new Dictionary<Tkey, Tvalue>(); ``` 2. 添加键值对:你可以使用`Add`方法来向Dictionary中添加键值对。例如: ``` dic.Add(key, value); ``` 3. 读取值:你可以使用索引器来读取Dictionary中特定键对应的值。例如: ``` print(dic[key]); ``` 4. 嵌套实例化:如果你需要在Dictionary中嵌套另一个Dictionary,你可以使用以下方式进行实例化和操作: ``` Dictionary<key, Dictionary<key, value>> dic = new Dictionary<key, Dictionary<key, value>>(); dic.Add(key, new Dictionary<key, value>()); // 实例化内嵌的字典 dic[key].Add(key, value); // 添加内层值 dic[key = new Dictionary<key, value>(); // 给外层的某个值赋值字典值 print(dic[key][key]); // 读取嵌套字典里的某个值 ``` 通过上述方法,你可以在Unity中使用Dictionary来管理键值对的数据。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [Unity-SerializableDictionaryUnity的可序列化字典类](https://download.csdn.net/download/weixin_42143092/15104075)[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^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [unity c# dictionary字典用法,dictionary嵌套用法。](https://blog.csdn.net/u011644138/article/details/81562606)[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^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值