C# 数据结构与底层原理

1,199 阅读22分钟

数组

  • 数组是由一个变量名称表示的一组同类型的数据元素。

  • 数组继承自System.Array。数组是引用类型。引用在堆或者栈上,而数组对象本身总是在堆上。

IMG_0452(20240326-222146).PNG

  • 尽管数组总是引用类型,但数组的元素可以是值类型也可以是引用类型。

75BF1C1E8577EB3B266A579E8AE50AE6.png

  • 冷知识:矩形数组和交错数组。比如3×3的矩形数组只有一个数组对象,3个长度不一致的一维数组构成一个交错数组。

List

《Unity3D高级编程之进阶主程》第一章,C#要点技术(一) - List 底层源码剖析 - 技术人生 (luzexi.com)

👉list源码👈

构造

public class List<T> : IList<T>, System.Collections.IList, IReadOnlyList<T>
{
    private const int _defaultCapacity = 4;
​
    private T[] _items; // 数组
    private int _size;
    private int _version;
    private Object _syncRoot;
    
    static readonly T[]  _emptyArray = new T[0];        
        
    // Constructs a List. The list is initially empty and has a capacity
    // of zero. Upon adding the first element to the list the capacity is
    // increased to 16, and then increased in multiples of two as required.
    public List() {
        _items = _emptyArray;
    }
​
    // Constructs a List with a given initial capacity. The list is
    // initially empty, but will have room for the given number of elements
    // before any reallocations are required.
    // 
    public List(int capacity) {
        if (capacity < 0) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
        Contract.EndContractBlock();
​
        if (capacity == 0)
            _items = _emptyArray;
        else
            _items = new T[capacity]; // 本质上是数组
    }
​
    //...
    //其他内容
}
​

这个类继承了两个接口,顺便看一下:

IList源码

public interface IList : ICollection
{
    // The Item property provides methods to read and edit entries in the List.
    Object this[int index] {
        get;
        set;
    }
...    

对这个写法感到好奇,继续扒List的源码看如何使用👇

Object System.Collections.IList.this[int index] {
    get {
        return this[index];
    }
    set {
        ThrowHelper.IfNullAndNullsAreIllegalThenThrow<T>(value, ExceptionArgument.value);

        try { 
            this[index] = (T)value;               
        }
        catch (InvalidCastException) { 
            ThrowHelper.ThrowWrongValueTypeArgumentException(value, typeof(T));            
        }
    }
}

原来是C# 索引器(Indexer)索引器(Indexer)  允许一个对象可以像数组一样使用下标的方式来访问。相当于使用数组访问运算符 [ ] 来访问该类的成员。也就是List使用了索引器,行为更像数组了。

❓变量_version有什么用?只有_version++处理,没有其它访问??一般在数组变更时出现。

IReadOnly源码

public interface IReadOnlyList<out T> : IReadOnlyCollection<T>
{
    T this[int index] { get; } // list找不到实现??急
}
  • List 继承于IList,IReadOnlyList;IList提供了主要的接口(Add/Contains/Clear等方法),IReadOnlyList只提供了访问索引器。
  • List内部是存有一个数组变量。

🥹关于List初始化的一个小坑:
原以为new List<T>(count)规定容量大小也就是确定了数组长度,想直接通过索引访问,但报错;

虽然源码层面确实是开辟了这么长的数组,但_size这个变量并不更新,而this[int index]就是用_size来比较索引访问是否合法的,因此就是报错。

_size只有Add/Remove/AddRange/RemoveRange等方法才会更新,所以初始化还是老实Add或者={...}

new List<T>(count)有什么用呢?改变数组长度的时候就是通过_items.Length来比较的,相当于提前开辟了这么大的空间,免得频繁扩容。

Add

// Adds the given object to the end of this list. The size of the list is
// increased by one. If required, the capacity of the list is doubled
// before adding the new element.
//
public void Add(T item) {
    if (_size == _items.Length) EnsureCapacity(_size + 1);
    _items[_size++] = item;
    _version++;
}
​
// Ensures that the capacity of this list is at least the given minimum
// value. If the currect capacity of the list is less than min, the
// capacity is increased to twice the current capacity or to min,
// whichever is larger.
private void EnsureCapacity(int min) {
// 原数组长度的两倍,并进行大小限定
    if (_items.Length < min) {
        int newCapacity = _items.Length == 0? _defaultCapacity : _items.Length * 2;
        // Allow the list to grow to maximum possible capacity (~2G elements) before encountering overflow.
        // Note that this check works even when _items.Length overflowed thanks to the (uint) cast
        if ((uint)newCapacity > Array.MaxArrayLength) newCapacity = Array.MaxArrayLength;
        if (newCapacity < min) newCapacity = min;
        Capacity = newCapacity;
    }
}

// Gets and sets the capacity of this list.  The capacity is the size of
// the internal array used to hold items.  When set, the internal 
// array of the list is reallocated to the given capacity.
// 
public int Capacity {
    get {
        Contract.Ensures(Contract.Result<int>() >= 0);
        return _items.Length;
    }
    set {
        if (value < _size) {
            ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_SmallCapacity);
        }
        Contract.EndContractBlock();

        if (value != _items.Length) {
            if (value > 0) {
                T[] newItems = new T[value];
                // 只要改变容量,就将原数组复制到新数组去
                if (_size > 0) {
                    Array.Copy(_items, 0, newItems, 0, _size);
                }
                _items = newItems;
            }
            else {
                _items = _emptyArray;
            }
        }
    }
}
  • 每次增加一个元素的数据,Add接口都会首先检查的是容量还够不够,如果不够则用 EnsureCapacity 来增加容量。

  • 每次容量不够的时候,整个数组的容量都会扩充一倍,_defaultCapacity 是容量的默认值为4。因此整个扩充的路线为4,8,16,32,64,128,256,512,1024…依次类推。

    List使用数组形式作为底层数据结构,好处是使用索引方式提取元素很快,但在扩容的时候就会很糟糕,每次new数组都会造成内存垃圾,这给垃圾回收GC带来了很多负担。

    这里按2指数扩容的方式,可以为GC减轻负担,但是如果当数组连续被替换掉也还是会造成GC的不小负担,特别是代码中List频繁使用的Add时。

    另外,如果数量不得当也会浪费大量内存空间,比如当元素数量为 520(512刚好不够,却只能两倍扩充) 时,List 就会扩容到1024个元素,如果不使用剩余的504个空间单位,就造成了大部分的内存空间的浪费。

Remove

// Removes the element at the given index. The size of the list is
// decreased by one.
// 
public bool Remove(T item) {
    int index = IndexOf(item);
    if (index >= 0) {
        RemoveAt(index);
        return true;
    }
​
    return false;
}
​
// Returns the index of the first occurrence of a given value in a range of
// this list. The list is searched forwards from beginning to end.
// The elements of the list are compared to the given value using the
// Object.Equals method.
// 
// This method uses the Array.IndexOf method to perform the
// search.
// 
public int IndexOf(T item) {
    Contract.Ensures(Contract.Result<int>() >= -1);
    Contract.Ensures(Contract.Result<int>() < Count);
    return Array.IndexOf(_items, item, 0, _size);
}
​
// Removes the element at the given index. The size of the list is
// decreased by one.
// 
public void RemoveAt(int index) {
    if ((uint)index >= (uint)_size) {
        ThrowHelper.ThrowArgumentOutOfRangeException();
    }
    Contract.EndContractBlock();
    _size--;
    if (index < _size) {
        Array.Copy(_items, index + 1, _items, index, _size - index);
    }
    _items[_size] = default(T);
    _version++;
}
  • IndexOf 启用的是 Array.IndexOf 接口来查找元素的索引位置,这个接口本身内部实现是就是按索引顺序从0到n对每个位置的比较,复杂度为O(n)。
  • 元素删除的原理其实就是用 Array.Copy 对数组进行覆盖

Insert

// Inserts an element into this list at a given index. The size of the list
// is increased by one. If required, the capacity of the list is doubled
// before inserting the new element.
// 
public void Insert(int index, T item) {
    // Note that insertions at the end are legal.
    if ((uint) index > (uint)_size) {
        ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, ExceptionResource.ArgumentOutOfRange_ListInsert);
    }
    Contract.EndContractBlock();
    if (_size == _items.Length) EnsureCapacity(_size + 1);
    if (index < _size) {
        Array.Copy(_items, index, _items, index + 1, _size - index);
    }
    _items[index] = item;
    _size++;            
    _version++;
}
  • 与Add接口一样,先检查容量是否足够,不足则扩容。
  • 从源码中获悉,Insert插入元素时,使用的用拷贝数组的形式,将数组里的指定元素后面的元素向后移动一个位置。

List的Add,Insert,IndexOf,Remove接口都是没有做过任何形式的优化,都使用的是顺序迭代的方式,如果过于频繁使用的话,会导致效率降低,也会造成不少内存的冗余,使得垃圾回收(GC)时承担了更多的压力。

其他相关接口比如 AddRange,RemoveRange的原理和Add与Remove一样,区别只是多了几个元素,把单个元素变成了以容器为单位的形式进行操作。都是先检查容量是否合适,不合适则扩容,或者当Remove时先得到索引位置再进行整体的覆盖掉后面的的元素,容器本身大小不会变化,只是做了重复覆盖的操作。

Clear

// Clears the contents of List.
public void Clear() {
    if (_size > 0)
    {
        Array.Clear(_items, 0, _size); // Don't need to doc this but we clear the elements so that the gc can reclaim the references.
        _size = 0;
    }
    _version++;
}

Clear接口在调用时并不会删除数组,而只是将数组中的元素清零,并设置 _size 为 0 而已,用于虚拟地表明当前容量为0。

分析

  • 其它

    Sort:用的是快排算法;

  • 【效率低】

    List 的效率并不高,只是通用性强而已,大部分的算法都使用的是线性复杂度的算法,这种线性算法当遇到规模比较大的计算量级时就会导致CPU的大量损耗。

    可以自己改进它,比如不再使用有线性算法的接口,自己重写一套,但凡要优化List 中的线性算法的地方都使用,我们自己制作的工具类。

  • 【内存分配方式】

    List的内存分配方式也极为不合理,当List里的元素不断增加时,会多次重新new数组,导致原来的数组被抛弃,最后当GC被调用时造成回收的压力。

    我们可以提前告知 List 对象最多会有多少元素在里面,这样的话 List 就不会因为空间不够而抛弃原有的数组,去重新申请数组了。

  • 【线程安全】

    代码是线程不安全的,它并没有对多线程下做任何锁或其他同步操作。并发情况下,无法判断 _size++ 的执行顺序,因此当我们在多线程间使用 List 时加上安全机制。

如何在遍历的时候删除list中的元素

C#遍历List并删除某个或者几个元素的方法 - 一夜秋2014 - 博客园 (cnblogs.com)

  • foreach方法:foreach循环在迭代过程中依赖集合中的迭代器,而在删除元素时,会导致迭代器失效,进而导致异常的发生。

    这是一个常见的陷阱,因为foreach循环是为只读访问集合而设计的,其内部使用的迭代器在遍历过程中会对集合的状态进行检查。当集合的结构在遍历期间发生变化(例如删除元素),会导致迭代器的状态不一致,从而抛出InvalidOperationException异常。

  • for正序遍历删除:由于删除之后后面元素往前补齐,而且i++,就会跳过;

  • for逆序遍历删除:可正确得到结果,但逆序添加元素又不同了。

List的浅复制和深复制

【T是值类型的情况】

  • 浅复制
List< T > oldList = new List< T >();
oldList.Add(…);
List< T > newList = oldList;    // 直接赋值
  • 深复制
List< T > oldList = new List< T >();
oldList.Add(…);
List< T > newList = new List< T >(oldList);	// 构造函数传参
  • 构造传参源码:
// Constructs a List, copying the contents of the given collection. The
// size and capacity of the new list will both be equal to the size of the
// given collection.
// 
public List(IEnumerable<T> collection) {
    if (collection==null)
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection);
    Contract.EndContractBlock();

    ICollection<T> c = collection as ICollection<T>;
    if( c != null) {
        int count = c.Count;
        if (count == 0)
        {
            _items = _emptyArray;
        }
        else {
            _items = new T[count]; // 将数组新对象的引用赋值给当前数组变量,然后一一赋值
            c.CopyTo(_items, 0); // 元素一一copy
            _size = count;
        }
    }    
    else {                
        _size = 0;
        _items = _emptyArray;
        // This enumerable could be empty.  Let Add allocate a new array, if needed.
        // Note it will also go to _defaultCapacity first, not 1, then 2, etc.

        using(IEnumerator<T> en = collection.GetEnumerator()) {
            while(en.MoveNext()) {
                Add(en.Current);                                    
            }
        }
    }

Copy方法本质上是Array.Copy(),没翻到源码……如果从每个元素的角度来看,如果是引用类型赋值就是浅拷贝?这样的话下面提到的浅复制就说得通了。

【T不是值类型的情况】

T是引用类型或者是类的实例。

  • 浅复制
// 1.构造函数传参
// 【注意!!】如果oldList = new List<string>(){"a", "b"}; 也就是这样构造的话,效果是深复制!!
List< T > oldList = new List< T >();
oldList.Add(…);
List< T > newList = new List< T >(oldList);

// 2.调用复制函数(是否浅拷贝有待实践)
List< T > oldList = new List< T >();
oldList.Add(…);
T[] newList = new T[N];
oldList.CopyTo(newList);	
[CopyTo(T[], Int32))从目标数组的指定索引处开始,将整个 List 复制到兼容的一维数组。
CopyTo(Int32, T[], Int32, Int32)从目标数组的指定索引处开始,将元素的范围从 List 复制到兼容的一维数组。
CopyTo(T[])从目标数组的开头开始,将整个 List 复制到兼容的一维数组。
  • 深复制
static class Extensions
{
    public static IList<T> Clone<T>(this IList<T> listToClone) where T: ICloneable
    {
        return listToClone.Select(item => (T)item.Clone()).ToList();
    }
//当然前提是List中的对象要实现ICloneable接口
}

字典

源码:dictionary.cs (microsoft.com)

图解来源:浅析C# Dictionary实现原理 - InCerry - 博客园 (cnblogs.com)

【C#】浅析C# Dictionary实现原理 - 知乎 (zhihu.com)

底层数据结构

public class Dictionary<TKey,TValue>: IDictionary<TKey,TValue>, IDictionary, IReadOnlyDictionary<TKey, TValue>, ISerializable, IDeserializationCallback  {
    
    private struct Entry {
        public int hashCode;    // Lower 31 bits of hash code, -1 if unused
        public int next;        // Index of next entry, -1 if last
        public TKey key;           // Key of entry
        public TValue value;         // Value of entry
    }

    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;

    ...
  • 实现接口:IDictionary:ICollection-[]/Keys/Values/Contains/Add/Clear/Remove等。IReadOnlyDictionary-ContainsKey/TryGetValue/迭代器等。
  • 数组为底层数据结构。核心是buckets和entries数组。
  • 还有Entry类。

1548491185593

Add

一般用的构造器是这个。

public Dictionary(): this(0, null) {}

然后使用Add。

public void Add(TKey key, TValue value) {
    Insert(key, value, true);
}

核心是Insert方法。代码较多,标记重点再逐一分析。

private void Insert(TKey key, TValue value, bool add) {
        
    if( key == null ) {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
    }
    // 1.空间分配
    if (buckets == null) Initialize(0);
    // 2.计算哈希值
    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
    // In case we hit the collision threshold we'll need to switch to the comparer which is using randomized string hashing
    // in this case will be EqualityComparer<string>.Default.
    // Note, randomized string hashing is turned on by default on coreclr so EqualityComparer<string>.Default will 
    // be using randomized string hashing

    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

}

1.空间分配

  • 分析Initialize方法,size是为多少呢?传参给的是0,但应该要扩容。
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;
}
  • 查看GetPrime()方法,size是根据确定好的奇数来比较的。确保是奇数
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;
    }

    //outside of our predefined table. 
    //compute the hard way. 
    for (int i = (min | 1); i < Int32.MaxValue;i+=2) 
    {
        if (IsPrime(i) && ((i - 1) % Hashtable.HashPrime != 0))
            return i;
    }
    return min;
}
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, 6 2851, 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};
  • 那么扩容该如何计算呢?回到Initialize方法,有关于扩容的操作:
if (count == entries.Length)
{
    Resize();//
    targetBucket = hashCode % buckets.Length;
}
.....

private void Resize() {
    Resize(HashHelpers.ExpandPrime(count), false);
}
// Returns size of hashtable to grow to.
public static int ExpandPrime(int oldSize)
{
    int newSize = 2 * oldSize;//

    // Allow the hashtables to grow to maximum possible size (~2G elements) before encoutering capacity overflow.
    // Note that this check works even when _items.Length overflowed thanks to the (uint) cast
    if ((uint)newSize > MaxPrimeArrayLength && MaxPrimeArrayLength > oldSize)
    {
        Contract.Assert( MaxPrimeArrayLength == GetPrime(MaxPrimeArrayLength), "Invalid MaxPrimeArrayLength");
        return MaxPrimeArrayLength;
    }

    return GetPrime(newSize);
}


首先是把size扩容到两倍。接着继续Resize。两个数组都要更新
private void Resize(int newSize, bool forceNewHashCodes) {
    Contract.Assert(newSize >= entries.Length);
    
    // 新的bucket数组
    int[] newBuckets = new int[newSize];
    for (int i = 0; i < newBuckets.Length; i++) newBuckets[i] = -1;
    
    // 新的entry数组,拷贝
    Entry[] newEntries = new Entry[newSize];
    Array.Copy(entries, 0, newEntries, 0, count);
    
    if(forceNewHashCodes) {
        for (int i = 0; i < count; i++) {
            if(newEntries[i].hashCode != -1) {
                newEntries[i].hashCode = (comparer.GetHashCode(newEntries[i].key) & 0x7FFFFFFF);
            }
        }
    }
    
    // 长度改变,重新计算hashCode
    for (int i = 0; i < count; i++) {
        if (newEntries[i].hashCode >= 0) {
            int bucket = newEntries[i].hashCode % newSize;
            newEntries[i].next = newBuckets[bucket];
            newBuckets[bucket] = i;
        }
    }
    buckets = newBuckets;
    entries = newEntries;
}

了解的范围内,扩容有三种触发条件:

  • 初始化时未指定容量
  • 添加元素时超过当前容量
  • 加载因子过高:加载因子是衡量哈希表满载程度的指标,定义为当前元素数量 / 哈希桶数量。如果添加新元素导致加载因子超过一个预设的阈值(例如,加载因子过高可能会导致查找效率下降),即使当前元素数量没有超过内部数组的容量,也可能会触发扩容。这是为了分散哈希冲突,保持字典操作的效率。

源码中并没有具体的加载因子比较,而是也利用了第二种情况——判断entries是否已满,否则容量(包括entries和buckets)扩大两倍,这也确保了加载因子默认在0.5左右。


2.计算哈希值

int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
int targetBucket = hashCode % buckets.Length;
  • comparer.GetHashCode(key): 这部分调用了comparerGetHashCode方法来获取key的哈希码。在C#中,每个对象都有一个GetHashCode方法,该方法返回一个整型(int),表示该对象的哈希码。
  • & 0x7FFFFFFF: 这部分是位运算的“与”(AND)操作。0x7FFFFFFF是一个十六进制的常量,其二进制表示为01111111 11111111 11111111 11111111。这个特殊的值用于确保结果是一个非负整数。在C#中,int类型是一个32位有符号整数,其最高位(即第32位)是符号位,0表示正数,1表示负数。通过与0x7FFFFFFF进行AND操作,无论key的哈希码原本是正是负,最高位都会被强制置为0,从而确保结果是非负的。
  • 最后对哈希值做取余操作,以确定地址落在 Dictionary 数组长度范围内不会溢出。

3.确定存储位置

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;
    } 
}

哈希码相同-产生哈希碰撞(也就是计算的哈希桶相同),这可以用拉链法补救;但如果连key都一样,在add情况下是非法操作,不是add情况(比如写字典)就直接替换。

int index;
// freeCount记录了字典中被删除条目的数量
if (freeCount > 0) {
    //如果有空闲条目可用,freeList会指向这些空闲条目中的一个
    index = freeList;
    freeList = entries[index].next;
    freeCount--;
}
else {
    // count指当前条目的数量
    if (count == entries.Length)
    {
        Resize();
        targetBucket = hashCode % buckets.Length;
    }
    // count也是entries数组中第一个空闲位置的索引
    index = count;
    count++;
}

这段代码的主要作用是确定新键值对在内部数据结构中的存储位置。

  • 如果有已删除的条目空间可以重用,它会优先使用这些空间;
  • 如果没有,它会将新条目添加到entries数组的末尾。
  • 如果entries数组已满,它会先进行扩容操作。

这种设计旨在高效利用内存空间,同时保持字典操作的性能。

4.更新映射关系

entries[index].hashCode = hashCode;
// 头插法,新节点的下一个指向原来的头部,并被对应的哈希桶指为下一个
entries[index].next = buckets[targetBucket];
entries[index].key = key;
entries[index].value = value;
// 哈希桶指向entry的索引
buckets[targetBucket] = index;
version++;

在entries数组指定的索引赋值,相当于一个新节点。

递增字典的版本号version。这一步是为了支持在迭代字典时能够检测到集合的修改。如果在迭代过程中字典的内容被修改了(version发生变化),那么将抛出InvalidOperationException异常,以避免迭代器失效。

整体流程的图解

将键转化成hashcode之后,取余数,得到哈希值,找到哈希桶数组对应的位置2

将信息存入到entries数组中,0位是空的,就放到0处,将Entry的下标entryIndex赋值给buckets中对应下标的bucket。步骤3中是存放在entries[0]的位置,所以buckets[2]=0

1548492100757

  • 哈希碰撞:将新加入的元素放进entries的下一个空位,更新next和哈希桶指向的索引。

1548494357566

  • 删除的时候,如果元素在链表中间,就会让前一个指向后一个,空出来位置的用freelist记录,下一次可以接着用。不过这样就打乱了值插入的顺序。

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) {
            // 找到同key
            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;
                // 指向原freelist第一个
                entries[i].next = freeList;
                entries[i].key = default(TKey);
                entries[i].value = default(TValue);
                // 更新freelist
                freeList = i;
                freeCount++;
                // freelist只是int,相当于链表节点,长度由freeCount记录
                version++;
                return true;
            }
        }
    }
    return false;
}

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;
}

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;
}

[]

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);
    }
}

C#中哈希表和字典的区别

C#中哈希表和字典的区别|极客教程 (geek-docs.com)

c#哈希表和字典的区别c# dictionary和hashmap偏执灬的博客-CSDN博客[更详细]

  • 字典
// C# program to illustrate Dictionary
using System;
using System.Collections.Generic;
​
class GeekdocsDemo {
​
    static public void Main()
    {
        // Creating a dictionary
        // using Dictionary<TKey, TValue> class
        Dictionary<string, string> My_dict = new Dictionary<string, string>();
​
        // Adding key/value pairs in the Dictionary
        // Using Add() method
        My_dict.Add("a.01", "C");
        My_dict.Add("a.02", "C++");
        My_dict.Add("a.03", "C#");
​
        foreach(KeyValuePair<string, string> element in My_dict)
        {
            Console.WriteLine("Key:- {0} and Value:- {1}", element.Key, element.Value);
        }
    }
}
​
  • 哈希表
// C# program to illustrate a hashtable
using System;
using System.Collections;
​
class GeekdocsDemo {
​
    static public void Main()
    {
​
        // Create a hashtable
        // Using Hashtable class
        Hashtable my_hashtable = new Hashtable();
​
        // Adding key/value pair in the hashtable
        // Using Add() method
        my_hashtable.Add("A1", "Welcome");
        my_hashtable.Add("A2", "to");
        my_hashtable.Add("A3", "Geekdocsbai Geekdocsbai");
​
        foreach(DictionaryEntry element in my_hashtable)
        {
            Console.WriteLine("Key:- {0} and Value:- {1} ", element.Key, element.Value);
        }
    }
}
​
  • 区别
哈希表字典
Hashtable 是一个非泛型集合。字典是一个通用集合。
Hashtable 在 System.Collections 命名空间下定义。字典在 System.Collections.Generic 命名空间下定义。
在 Hashtable 中,可以存储相同类型或不同类型的键/值对。Dictionary 中,可以存储相同类型的键/值对。
在 Hashtable 中,不需要指定 key 和 value 的类型。在 Dictionary 中,必须指定键和值的类型。
由于装箱/拆箱,数据检索比字典慢。由于没有装箱/拆箱,数据检索比 Hashtable 快。
在 Hashtable 中,如果尝试访问给定 Hashtable 中不存在的键,那么它将给出 null 值。Dictionary 中,如果尝试访问给定 Dictionary 中不存在的键,则会出错。
它是线程安全的。它也是线程安全的,但仅适用于公共静态成员。
它不维护存储的顺序。它始终保持存储值的顺序。

Dictionary<Tkey,Tvalue>是Hastbale的泛型实现。

Dictionary和HashTable内部实现差不多,但前者无需装箱拆箱操作,效率略高一点。

  • 线程方面

1)单线程程序中推荐使用 Dictionary, 有泛型优势, 且读取速度较快, 容量利用更充分. 2)多线程程序中推荐使用 Hashtable, 默认的 Hashtable 允许单线程写入, 多线程读取, 对 Hashtable 进一步调用 Synchronized()方法可以获得完全线程安全的类型. 而Dictionary 非线程安全, 必须人为使用 lock 语句进行保护, 效率大减.

Foreach迭代是否产生GC

枚举器对象

  • Unity推荐使用foreach循环遍历实现了IEnumerable<T>的集合,确保这些集合的枚举器是结构体(struct),而不是类(class)。这是因为结构体枚举器在栈上分配,不会增加GC的压力。Unity的很多内置集合,如List<T>,其枚举器已经是结构体,使用foreach遍历这些集合通常不会导致GC。

  • 但如果遍历的是自定义集合,其枚举器类型是一个引用类型(class),那么每次使用 foreach 都会在堆上创建一个新的枚举器实例,这最终可能导致垃圾回收。

装箱操作

如果使用 foreach 遍历一个非泛型集合(比如 ArrayList)中的值类型数据,每次迭代都可能涉及到装箱操作,这会在堆上创建对象,从而增加垃圾回收的压力。

实验验证

【以下实验的Unity版本均为2021.3.8f1c1】

List<int> numbers = new List<int> { 1, 2 };
void Update()
{
    Profiler.BeginSample("ForeachGC");
    foreach (var x in numbers) { }
    Profiler.EndSample();
}

image.png

LinkedList

【c# .net】双向链表( LinkedList ) - 知乎 (zhihu.com)【使用详解】

双向链表。

img

1)LinkedList 无法通过下标查找元素,在查找链表元素时,总是从头结点开始查找。

2)LinkedList 的容量是链表最大包含的元素数,会根据元素增减而动态调整容量

3)LinkedList 中的每个节点 都属于 LinkedListNode 类型

4)LinkedList 的值可以为 null,并允许重复值

5)LinkedList 不自带排序方法。

  1. 查询复杂度为 O(n),操作复杂度为 O(1)。