数组
-
数组是由一个变量名称表示的一组同类型的数据元素。
-
数组继承自
System.Array。数组是引用类型。引用在堆或者栈上,而数组对象本身总是在堆上。
- 尽管数组总是引用类型,但数组的元素可以是值类型也可以是引用类型。
- 冷知识:矩形数组和交错数组。比如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]; // 本质上是数组
}
//...
//其他内容
}
这个类继承了两个接口,顺便看一下:
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++处理,没有其它访问??一般在数组变更时出现。
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类。
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): 这部分调用了comparer的GetHashCode方法来获取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。
- 哈希碰撞:将新加入的元素放进entries的下一个空位,更新next和哈希桶指向的索引。
- 删除的时候,如果元素在链表中间,就会让前一个指向后一个,空出来位置的用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】
-
根据这篇博客进行同样的实验,发现无论是哪种情况(遍历字典/多个字典/不同类型字典/只遍历字典keys或values)都没有产生GC。 Unity3D C# 中使用foreach的GC产出(2023年带数据)_unity foreach gc-CSDN博客
-
Unity/C# 漫谈一:foreach与GC - 知乎 (zhihu.com)这篇博客是2018年写的,指出了list在遍历值类型元素时产生了装箱操作导致GC,但我也测试了一下,并没有产生GC。 但是反编译而且查看IL代码的方法可以积累一下。
List<int> numbers = new List<int> { 1, 2 };
void Update()
{
Profiler.BeginSample("ForeachGC");
foreach (var x in numbers) { }
Profiler.EndSample();
}
LinkedList
【c# .net】双向链表( LinkedList ) - 知乎 (zhihu.com)【使用详解】
双向链表。
1)LinkedList 无法通过下标查找元素,在查找链表元素时,总是从头结点开始查找。
2)LinkedList 的容量是链表最大包含的元素数,会根据元素增减而动态调整容量。
3)LinkedList 中的每个节点 都属于 LinkedListNode 类型。
4)LinkedList 的值可以为 null,并允许重复值。
5)LinkedList 不自带排序方法。
- 查询复杂度为 O(n),操作复杂度为 O(1)。